Files
msd-core/tests/profile-output.test.cjs
Jakub Zych 6cfa0c55d2 refactor: drop 12 runtimes, keep Claude, Codex, OpenCode, Cursor, ZCode, Antigravity
Removes kilo, kimi, kimi-code, copilot, windsurf, augment, trae, qwen, hermes,
cline, codebuddy and pi end to end: capability descriptors, installer branches
and converters (bin/install.js 14.9k -> 11.2k lines), TypeScript converters,
hook surfaces and runtime homes, review lanes qwen/kimi-code, the two pi
migrations, Kimi payload normalization in the hook guards, dead hostBehaviors
vocabulary, launcher home probes, fixtures, runtime-specific tests and the
prose that presented them as supported.

Installer output for the six kept runtimes is byte-identical to before the
prune. The Kimi tool-vocabulary tests in workflow-guard, read-guard and
read-injection-scanner are left in place pending a decision.
2026-10-06 20:02:40 +02:00

534 lines
22 KiB
JavaScript

// allow-test-rule: source-text-is-the-product
// Reads .md/.json/.yml product files whose deployed text IS what the
// runtime loads — testing text content tests the deployed contract.
/**
* Profile Output Tests
*
* Tests for profile rendering commands and PROFILING_QUESTIONS data.
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const {
runMsdTools,
createTempProject,
createTempGitProject,
cleanup,
withIsolatedProcessState,
} = require('./helpers.cjs');
const {
PROFILING_QUESTIONS,
CLAUDE_INSTRUCTIONS,
} = require('../msd-core/bin/lib/profile-output.cjs');
// ─── PROFILING_QUESTIONS data ─────────────────────────────────────────────────
describe('PROFILING_QUESTIONS', () => {
test('is a non-empty array', () => {
assert.ok(Array.isArray(PROFILING_QUESTIONS));
assert.ok(PROFILING_QUESTIONS.length > 0);
});
test('each question has required fields', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(q.dimension, `question missing dimension`);
assert.ok(q.header, `${q.dimension} missing header`);
assert.ok(q.question, `${q.dimension} missing question`);
assert.ok(Array.isArray(q.options), `${q.dimension} options should be array`);
assert.ok(q.options.length >= 2, `${q.dimension} should have at least 2 options`);
}
});
test('each option has label, value, and rating', () => {
for (const q of PROFILING_QUESTIONS) {
for (const opt of q.options) {
assert.ok(opt.label, `${q.dimension} option missing label`);
assert.ok(opt.value, `${q.dimension} option missing value`);
assert.ok(opt.rating, `${q.dimension} option missing rating`);
}
}
});
test('all dimension keys are unique', () => {
const dims = PROFILING_QUESTIONS.map(q => q.dimension);
const unique = [...new Set(dims)];
assert.strictEqual(dims.length, unique.length);
});
});
// ─── CLAUDE_INSTRUCTIONS ──────────────────────────────────────────────────────
describe('CLAUDE_INSTRUCTIONS', () => {
test('is a non-empty object', () => {
assert.ok(typeof CLAUDE_INSTRUCTIONS === 'object');
assert.ok(Object.keys(CLAUDE_INSTRUCTIONS).length > 0);
});
test('each dimension has at least one instruction', () => {
for (const [dim, instructions] of Object.entries(CLAUDE_INSTRUCTIONS)) {
assert.ok(typeof instructions === 'object', `${dim} should be an object`);
assert.ok(Object.keys(instructions).length > 0, `${dim} should have instructions`);
}
});
test('every PROFILING_QUESTIONS dimension has CLAUDE_INSTRUCTIONS', () => {
for (const q of PROFILING_QUESTIONS) {
assert.ok(
CLAUDE_INSTRUCTIONS[q.dimension],
`${q.dimension} has questions but no CLAUDE_INSTRUCTIONS`
);
}
});
});
// ─── write-profile command ────────────────────────────────────────────────────
describe('write-profile command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('writes USER-PROFILE.md from analysis JSON', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
explanation_depth: { rating: 'concise', confidence: 'HIGH' },
debugging_approach: { rating: 'fix-first', confidence: 'LOW' },
ux_philosophy: { rating: 'function-first', confidence: 'MEDIUM' },
vendor_philosophy: { rating: 'pragmatic', confidence: 'HIGH' },
frustration_triggers: { rating: 'over-explanation', confidence: 'LOW' },
learning_style: { rating: 'hands-on', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runMsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir, { HOME: tmpDir });
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.profile_path, 'should return profile_path');
assert.ok(out.dimensions_scored > 0, 'should have scored dimensions');
});
test('#1114: default output resolves the active runtime config home (codex)', () => {
const analysis = {
profile_version: '1.0',
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
};
const analysisPath = path.join(tmpDir, 'analysis.json');
const codexHome = path.join(tmpDir, 'codex-home');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runMsdTools(
['write-profile', '--input', analysisPath, '--raw'],
tmpDir,
{ CODEX_HOME: codexHome, MSD_RUNTIME: 'codex' }
);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
// Must land in the Codex home (so Codex advisor-mode finds it), NOT .claude.
assert.strictEqual(out.profile_path, path.join(codexHome, 'msd-core', 'USER-PROFILE.md'));
assert.ok(!out.profile_path.includes(`${path.sep}.claude${path.sep}`),
`codex profile must not be written under .claude; got ${out.profile_path}`);
assert.ok(fs.existsSync(out.profile_path), 'runtime-aware profile should be written to disk');
});
test('#1114: default output is the .claude config home for the claude runtime', () => {
const analysis = {
profile_version: '1.0',
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
// Clear ambient runtime vars so the test is hermetic regardless of the
// developer's shell (a stray MSD_RUNTIME/CLAUDE_CONFIG_DIR would redirect it).
const result = runMsdTools(['write-profile', '--input', analysisPath, '--raw'], tmpDir,
{ HOME: tmpDir, MSD_RUNTIME: '', CLAUDE_CONFIG_DIR: '', CODEX_HOME: '' });
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
// os.homedir() returns HOME verbatim, so assert on the suffix to stay
// robust against macOS /var → /private/var symlink normalization.
assert.ok(
out.profile_path.endsWith(path.join('.claude', 'msd-core', 'USER-PROFILE.md')),
`claude profile must be under .claude/msd-core; got ${out.profile_path}`
);
assert.ok(fs.existsSync(out.profile_path), 'profile should be written to disk');
});
test('#1114: config.runtime=codex (no MSD_RUNTIME) also resolves the Codex home', () => {
const analysis = {
profile_version: '1.0',
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
};
const analysisPath = path.join(tmpDir, 'analysis.json');
const codexHome = path.join(tmpDir, 'codex-home');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
fs.writeFileSync(path.join(tmpDir, '.planning', 'config.json'), JSON.stringify({ runtime: 'codex' }));
// No MSD_RUNTIME — the runtime must be read from config.runtime.
const result = runMsdTools(
['write-profile', '--input', analysisPath, '--raw'],
tmpDir,
{ CODEX_HOME: codexHome, MSD_RUNTIME: '' }
);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.profile_path, path.join(codexHome, 'msd-core', 'USER-PROFILE.md'));
});
test('#2665: ambient CLAUDE_CONFIG_DIR cannot escape a HOME-only sandbox', () => {
// The defect: TEST_ENV_BASE blanked session-identity vars but none of the
// config-LOCATION vars, and the dot-home resolver is env-first. So an
// ambient CLAUDE_CONFIG_DIR in the DEVELOPER'S shell beat `{ HOME: tmpDir }`
// and the suite wrote into their real config directory. Setting it on the
// PARENT process is the actual vector — passing it in the per-call env
// argument would test nothing, because that path was never broken.
const analysis = {
profile_version: '1.0',
dimensions: { communication_style: { rating: 'terse-direct', confidence: 'HIGH' } },
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const ambientConfigDir = path.join(tmpDir, 'ambient-live-config');
fs.mkdirSync(ambientConfigDir, { recursive: true });
const out = withIsolatedProcessState(() => {
process.env.CLAUDE_CONFIG_DIR = ambientConfigDir;
const result = runMsdTools(
['write-profile', '--input', analysisPath, '--raw'],
tmpDir,
{ HOME: tmpDir }
);
assert.ok(result.success, `Failed: ${result.error}`);
return JSON.parse(result.output);
});
assert.deepStrictEqual(
fs.readdirSync(ambientConfigDir),
[],
'a call site that sandboxes HOME must not write into an ambient CLAUDE_CONFIG_DIR'
);
// Containment via path.relative, not startsWith: startsWith(ambientConfigDir)
// also matches a SIBLING like `<tmp>/ambient-live-config-2`, so it can report
// a false leak. A path is inside the dir iff the relative path neither escapes
// with '..' nor is absolute.
const rel = path.relative(ambientConfigDir, out.profile_path);
assert.ok(
rel.startsWith('..') || path.isAbsolute(rel),
`profile must not resolve under the ambient config dir, got: ${out.profile_path}`
);
});
test('errors when --input is missing', () => {
const result = runMsdTools('write-profile --raw', tmpDir);
assert.ok(!result.success, 'should fail without --input');
assert.ok(result.error.includes('--input'), 'should mention --input');
});
});
// ─── generate-claude-md command ───────────────────────────────────────────────
describe('generate-claude-md command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempGitProject();
fs.writeFileSync(
path.join(tmpDir, '.planning', 'PROJECT.md'),
'# My Project\n\nA test project.\n\n## Tech Stack\n\n- Node.js\n- TypeScript\n'
);
});
afterEach(() => {
cleanup(tmpDir);
});
test('generates CLAUDE.md with --auto flag', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
const result = runMsdTools(['generate-claude-md', '--output', outputPath, '--auto', '--raw'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
if (fs.existsSync(outputPath)) {
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.length > 0, 'should have content');
}
});
test('does not overwrite existing marker-less CLAUDE.md without --force (#1098)', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
const original = '# Custom CLAUDE.md\n\nUser content.\n';
fs.writeFileSync(outputPath, original);
// No MSD markers in the file → the #1098 guard must leave it untouched.
const result = runMsdTools(['generate-claude-md', '--output', outputPath, '--auto'], tmpDir);
assert.ok(result.success, `command should exit 0 even when skipping: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).action, 'skipped');
const content = fs.readFileSync(outputPath, 'utf-8');
assert.strictEqual(content, original, 'hand-crafted file must be byte-identical (not overwritten)');
});
test('overwrites existing marker-less CLAUDE.md with --force (#1098)', () => {
const outputPath = path.join(tmpDir, 'CLAUDE.md');
fs.writeFileSync(outputPath, '# Custom CLAUDE.md\n\nUser content.\n');
const result = runMsdTools(['generate-claude-md', '--output', outputPath, '--force'], tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
assert.strictEqual(JSON.parse(result.output).action, 'updated');
const content = fs.readFileSync(outputPath, 'utf-8');
assert.ok(content.includes('User content.'), '--force preserves existing content while adding sections');
assert.ok(content.includes('## MSD Workflow Enforcement'), '--force injects MSD sections');
});
test('skills fallback mentions the normalized project roots', () => {
const result = runMsdTools('generate-claude-md', tmpDir);
assert.ok(result.success, `Failed: ${result.error}`);
// #1098: default Claude output is now .claude/CLAUDE.md
const content = fs.readFileSync(path.join(tmpDir, '.claude', 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('.claude/skills/'));
assert.ok(content.includes('.agents/skills/'));
assert.ok(content.includes('.cursor/skills/'));
assert.ok(content.includes('.github/skills/'));
assert.ok(content.includes('.codex/skills/'));
assert.ok(!content.includes('msd-core/skills'));
});
test('codex runtime aliases default output to AGENTS.md', () => {
const result = runMsdTools(
['generate-claude-md', '--auto', '--raw'],
tmpDir,
{ MSD_RUNTIME: 'codex-cli' }
);
assert.ok(result.success, `Failed: ${result.error}`);
assert.ok(fs.existsSync(path.join(tmpDir, 'AGENTS.md')), 'AGENTS.md should be generated for codex aliases');
});
});
// ─── generate-dev-preferences ─────────────────────────────────────────────────
describe('generate-dev-preferences command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('errors when --analysis is missing', () => {
const result = runMsdTools('generate-dev-preferences --raw', tmpDir);
assert.ok(!result.success, 'should fail without --analysis');
assert.ok(result.error.includes('--analysis'), 'should mention --analysis');
});
test('generates preferences from analysis file', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
decision_speed: { rating: 'fast-intuitive', confidence: 'MEDIUM' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runMsdTools(
['generate-dev-preferences', '--analysis', analysisPath, '--raw'],
tmpDir,
{ HOME: tmpDir }
);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.ok(out.command_path || out.command_name, 'should return command output');
});
test('uses runtime-aware skills dir for codex by default', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
const codexHome = path.join(tmpDir, 'codex-home');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runMsdTools(
['generate-dev-preferences', '--analysis', analysisPath, '--raw'],
tmpDir,
// #2088 (ADR-1239 upgrade 3): Codex skills resolve to $HOME/.agents/skills
// (HOME-relative), so sandbox HOME to keep the dev-preferences write inside
// the temp dir rather than the developer's real ~/.agents/skills.
{ CODEX_HOME: codexHome, MSD_RUNTIME: 'codex', HOME: codexHome, USERPROFILE: codexHome }
);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.command_path, path.join(codexHome, '.agents', 'skills', 'msd-dev-preferences', 'SKILL.md'));
assert.ok(fs.existsSync(out.command_path), 'runtime-aware output should be written');
});
test('canonicalizes codex runtime aliases for skills output path', () => {
const analysis = {
profile_version: '1.0',
dimensions: {
communication_style: { rating: 'terse-direct', confidence: 'HIGH' },
},
};
const analysisPath = path.join(tmpDir, 'analysis.json');
const codexHome = path.join(tmpDir, 'codex-home');
fs.writeFileSync(analysisPath, JSON.stringify(analysis));
const result = runMsdTools(
['generate-dev-preferences', '--analysis', analysisPath, '--raw'],
tmpDir,
// #2088: codex-app alias canonicalizes to codex → $HOME/.agents/skills.
{ CODEX_HOME: codexHome, MSD_RUNTIME: 'codex-app', HOME: codexHome, USERPROFILE: codexHome }
);
assert.ok(result.success, `Failed: ${result.error}`);
const out = JSON.parse(result.output);
assert.strictEqual(out.command_path, path.join(codexHome, '.agents', 'skills', 'msd-dev-preferences', 'SKILL.md'));
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/enh-2415-claude-md-link-mode.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:enh-2415-claude-md-link-mode (consolidation epic #1969 B3 #1972)", () => {
'use strict';
// allow-test-rule: source-text-is-the-product (see #2415)
// Reads .md/.json/.yml product files whose deployed text IS what the
// runtime loads — testing text content tests the deployed contract.
/**
* Tests for claude_md_assembly "link" mode (#2415).
* Verifies that generate-claude-md writes @-references instead of inlined
* content when claude_md_assembly.mode is "link".
*/
const { test, after } = 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 helpers = require('./helpers.cjs');
const { cmdGenerateClaudeMd } = require('../msd-core/bin/lib/profile-output.cjs');
const _dirsToClean = [];
after(() => { for (const d of _dirsToClean) helpers.cleanup(d); });
function makeTempProject(files = {}) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2415-'));
_dirsToClean.push(dir);
fs.mkdirSync(path.join(dir, '.planning', 'codebase'), { recursive: true });
for (const [rel, content] of Object.entries(files)) {
const abs = path.join(dir, rel);
fs.mkdirSync(path.dirname(abs), { recursive: true });
fs.writeFileSync(abs, content, 'utf-8');
}
return dir;
}
test('link mode writes @-reference for architecture section', () => {
const dir = makeTempProject({
'.planning/codebase/ARCHITECTURE.md': '# Architecture\n\n- layered\n',
'.planning/config.json': JSON.stringify({ claude_md_assembly: { mode: 'link' } }),
});
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('@.planning/codebase/ARCHITECTURE.md'), 'should contain @-reference');
assert.ok(!content.includes('- layered'), 'should not inline architecture content');
});
test('link mode writes @-reference for project section', () => {
const dir = makeTempProject({
'.planning/PROJECT.md': '# My Project\n\n## What This Is\n\nA great app.\n',
'.planning/config.json': JSON.stringify({ claude_md_assembly: { mode: 'link' } }),
});
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('@.planning/PROJECT.md'), 'should contain @-reference for project');
assert.ok(!content.includes('A great app.'), 'should not inline project content');
});
test('embed mode (default) inlines content as before', () => {
const dir = makeTempProject({
'.planning/codebase/ARCHITECTURE.md': '# Architecture\n\n- monolith\n',
});
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('- monolith'), 'embed mode should inline content');
assert.ok(!content.includes('@.planning/codebase/ARCHITECTURE.md'), 'embed mode should not write @-reference');
});
test('per-block override: link only architecture, embed others', () => {
const dir = makeTempProject({
'.planning/PROJECT.md': '# Proj\n\n## What This Is\n\nApp.\n',
'.planning/codebase/ARCHITECTURE.md': '# Arch\n\n- layers\n',
'.planning/config.json': JSON.stringify({ claude_md_assembly: { mode: 'embed', blocks: { architecture: 'link' } } }),
});
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
assert.ok(content.includes('@.planning/codebase/ARCHITECTURE.md'), 'architecture should use link');
assert.ok(!content.includes('@.planning/PROJECT.md'), 'project should use embed');
assert.ok(content.includes('App.'), 'project content should be inlined');
});
test('link mode falls back to embed for workflow section (no linkable source)', () => {
const dir = makeTempProject({
'.planning/config.json': JSON.stringify({ claude_md_assembly: { mode: 'link' } }),
});
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
// workflow section should still be inlined (it has no linkPath)
assert.ok(!content.includes('@MSD defaults'), 'workflow should not write @MSD defaults');
assert.ok(content.includes('MSD Workflow Enforcement'), 'workflow content should be embedded inline');
});
test('link mode falls back to embed when source file is missing (hasFallback)', () => {
const dir = makeTempProject({
'.planning/config.json': JSON.stringify({ claude_md_assembly: { mode: 'link' } }),
});
// No .planning/codebase/ARCHITECTURE.md — generator will use fallback
cmdGenerateClaudeMd(dir, { output: path.join(dir, 'CLAUDE.md') }, false);
const content = fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf-8');
assert.ok(!content.includes('@.planning/codebase/ARCHITECTURE.md'), 'fallback section should not write @-reference');
});
});
}