Files
msd-core/tests/docs-update.test.cjs
Tom Boucher b6e6a22fce fix(#2402): honor response_language across orchestrator output + UAT checkpoint renderer (#2457)
* fix(#2402): honor response_language across orchestrator output + UAT checkpoint renderer

Replays the in-flight bot branch fix/2402-response-language-orchestrator-coverage
(seven commits, never pushed) onto current origin/next as a single squashed commit.
The original work was substantial and correct; this commit preserves its full scope,
trimmed where rebase conflicts + workflow size budgets required it.

Three independent layers where response_language was being dropped are closed:

Layer 1 — orchestrator-facing directives across workflows. Adds the strong
"All user-facing output in this workflow MUST be presented in {response_language};
technical terms, code, paths, and subagent prompts stay in English" directive
to ~40 workflows that previously either lacked it entirely (verify-work,
new-project, new-milestone, quick, manager, and ~35 more) or carried only
the weak subagent-prompt-only form (plan-phase, execute-phase). The directive
covers narration between tool calls and banner output, not just the
AskUserQuestion prompts.

Layer 2 — UAT checkpoint renderer (src/uat.cts). buildCheckpoint now accepts
an optional responseLanguage parameter and renders the frame strings
("CHECKPOINT: Verification Required", "Type `pass` or describe what's wrong.")
in any of 9 languages (English/Spanish/French/German/Portuguese/Japanese/
Chinese/Korean/Italian) with an alias table covering ~30 input variants
(en, es, español, ja, 日本語, etc.). cmdRenderCheckpoint reads
config.response_language via loadConfig(cwd) and passes it through, so the
byte-for-byte block verify-work.md reprints verbatim is already localized
when written — preserving the anti-injection hygiene rule at verify-work.md
(the model is forbidden to translate after the fact). CJK display width is
computed by East Asian Width property ranges (W/F) so the right ║ border of
the banner stays aligned for full-width characters. English fallback is
byte-identical to the pre-fix behavior when response_language is unset or
unrecognized.

Layer 3 — literal English report templates in execute-phase. The top-of-
workflow directive covers all template sites (templates are a structural
source, not literal output). Inline render-language notes that previously
sat at each template site were removed during the squash because they
pushed execute-phase.md over its frozen pre-phase-6 byte ceiling
(93600 — ADR-857 Phase 6 capstone). The single top directive covers the
same surface with fewer bytes.

Also extends src/docs.cts and src/init.cts to propagate response_language
into the init JSON bundle of the additional workflows so the directive can
read it.

Tests added:
- tests/uat.test.cjs: buildCheckpoint with unset/unrecognized language falls
  back to English default; recognized language swaps only the two frame
  strings while structural lines stay untouched; CJK display-width regression
  (independent recomputation of East Asian Width W/F ranges).
- tests/workspace.test.cjs, tests/docs-update.test.cjs: response_language
  wiring through docs.cts/init.cts.

References: #2402; reporter's three-layer triage + Layer-4 follow-up; the
byte-for-byte anti-injection hygiene rule at verify-work.md (the reason
Layer 2 must be renderer-side, not model-translated).

This is a squash of the in-flight bot branch — seven commits representing
the original implementation plus its subsequent fix/CJK-padding/test/
changeset/regen cycles, none of which were ever pushed or PR'd. The squash
captures the final coherent state.

* chore(#2402): backfill pr:2457 in .changeset/2402-response-language-orchestrator-coverage.md

* chore(#2402): regen golden + size baseline after rebase against #2315 (PR #2451)

Rebase conflicts were entirely in generated artifacts (golden-install-parity
fixtures + workflow-size-baseline.json). After taking theirs during rebase,
regenerated cleanly against the merged source tree.
2026-07-20 14:22:27 -04:00

454 lines
18 KiB
JavaScript

/**
* GSD Tools Tests - docs-update
*
* Integration tests for the docs-init gsd-tools subcommand.
* Covers: JSON output shape, project type detection, existing doc scanning,
* GSD marker detection, and doc tooling detection.
*
* Requirements: VERF-03
*/
const { test, describe, beforeEach, afterEach } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
// ─── JSON output shape ────────────────────────────────────────────────────────
describe('docs-init command', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('returns expected JSON shape', () => {
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
// Top-level scalar fields
assert.strictEqual(typeof data.doc_writer_model, 'string');
assert.strictEqual(typeof data.commit_docs, 'boolean');
assert.strictEqual(typeof data.planning_exists, 'boolean');
assert.strictEqual(typeof data.project_root, 'string');
assert.strictEqual(typeof data.agents_installed, 'boolean');
// Array fields
assert.ok(Array.isArray(data.existing_docs), 'existing_docs should be an array');
assert.ok(Array.isArray(data.monorepo_workspaces), 'monorepo_workspaces should be an array');
assert.ok(Array.isArray(data.missing_agents), 'missing_agents should be an array');
// project_type object with 7 boolean fields
assert.ok(data.project_type && typeof data.project_type === 'object', 'project_type should be an object');
assert.strictEqual(typeof data.project_type.has_package_json, 'boolean');
assert.strictEqual(typeof data.project_type.has_api_routes, 'boolean');
assert.strictEqual(typeof data.project_type.has_cli_bin, 'boolean');
assert.strictEqual(typeof data.project_type.is_open_source, 'boolean');
assert.strictEqual(typeof data.project_type.has_deploy_config, 'boolean');
assert.strictEqual(typeof data.project_type.is_monorepo, 'boolean');
assert.strictEqual(typeof data.project_type.has_tests, 'boolean');
// doc_tooling object with 4 boolean fields
assert.ok(data.doc_tooling && typeof data.doc_tooling === 'object', 'doc_tooling should be an object');
assert.strictEqual(typeof data.doc_tooling.docusaurus, 'boolean');
assert.strictEqual(typeof data.doc_tooling.vitepress, 'boolean');
assert.strictEqual(typeof data.doc_tooling.mkdocs, 'boolean');
assert.strictEqual(typeof data.doc_tooling.storybook, 'boolean');
// planning_exists is true since createTempProject creates .planning/
assert.strictEqual(data.planning_exists, true);
});
test('bare project returns all false signals', () => {
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
// All project_type fields should be false for a bare project
assert.strictEqual(data.project_type.has_package_json, false);
assert.strictEqual(data.project_type.has_api_routes, false);
assert.strictEqual(data.project_type.has_cli_bin, false);
assert.strictEqual(data.project_type.is_open_source, false);
assert.strictEqual(data.project_type.has_deploy_config, false);
assert.strictEqual(data.project_type.is_monorepo, false);
assert.strictEqual(data.project_type.has_tests, false);
// No docs, no workspaces, no doc tooling
assert.deepEqual(data.existing_docs, []);
assert.deepEqual(data.monorepo_workspaces, []);
assert.strictEqual(data.doc_tooling.docusaurus, false);
assert.strictEqual(data.doc_tooling.vitepress, false);
assert.strictEqual(data.doc_tooling.mkdocs, false);
assert.strictEqual(data.doc_tooling.storybook, false);
});
});
// ─── response_language wiring (#2402) ─────────────────────────────────────────
//
// cmdDocsInit predates withProjectRoot (init.cts) and never picked up
// response_language, so docs-update's orchestrator-owned prompts silently
// stayed English even with response_language configured. Mirrors the
// established `init manager` response_language coverage pattern
// (tests/init-manager.test.cjs).
describe('docs-init response_language wiring (#2402)', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('output includes response_language when configured', () => {
fs.writeFileSync(
path.join(tmpDir, '.planning', 'config.json'),
JSON.stringify({ response_language: 'Japanese' })
);
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.response_language, 'Japanese');
});
test('output omits response_language when not configured', () => {
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.response_language, undefined);
});
});
// ─── project type detection ───────────────────────────────────────────────────
describe('project type detection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('detects CLI tool from package.json bin field', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ name: 'my-cli', bin: { mycli: 'bin/cli.js' } }),
'utf-8'
);
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_cli_bin, true);
assert.strictEqual(data.project_type.has_package_json, true);
});
test('detects open source from LICENSE file', () => {
fs.writeFileSync(path.join(tmpDir, 'LICENSE'), 'MIT License', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.is_open_source, true);
});
test('detects monorepo from package.json workspaces', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({ name: 'mono', workspaces: ['packages/*'] }),
'utf-8'
);
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.is_monorepo, true);
assert.ok(data.monorepo_workspaces.includes('packages/*'), 'monorepo_workspaces should contain packages/*');
});
test('detects tests from tests directory', () => {
fs.mkdirSync(path.join(tmpDir, 'tests'), { recursive: true });
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_tests, true);
});
test('detects deploy config from Dockerfile', () => {
fs.writeFileSync(path.join(tmpDir, 'Dockerfile'), 'FROM node:20', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_deploy_config, true);
});
test('detects API routes from src/app/api directory', () => {
fs.mkdirSync(path.join(tmpDir, 'src', 'app', 'api'), { recursive: true });
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.project_type.has_api_routes, true);
});
});
// ─── existing doc scanning ────────────────────────────────────────────────────
describe('existing doc scanning', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('scans .md files in project root', () => {
fs.writeFileSync(path.join(tmpDir, 'README.md'), '# README\n', 'utf-8');
fs.writeFileSync(path.join(tmpDir, 'ARCHITECTURE.md'), '# Architecture\n', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.ok(data.existing_docs.length >= 2, 'existing_docs should contain at least 2 entries');
const paths = data.existing_docs.map(d => d.path);
assert.ok(paths.includes('README.md'), 'existing_docs should contain README.md');
assert.ok(paths.includes('ARCHITECTURE.md'), 'existing_docs should contain ARCHITECTURE.md');
});
test('detects GSD marker in existing docs', () => {
fs.writeFileSync(
path.join(tmpDir, 'README.md'),
'<!-- generated-by: gsd-doc-writer -->\n# README\n',
'utf-8'
);
fs.writeFileSync(path.join(tmpDir, 'NOTES.md'), '# Notes\n', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
const readmeEntry = data.existing_docs.find(d => d.path === 'README.md');
assert.ok(readmeEntry, 'README.md should appear in existing_docs');
assert.strictEqual(readmeEntry.has_gsd_marker, true, 'README.md should have GSD marker');
const notesEntry = data.existing_docs.find(d => d.path === 'NOTES.md');
assert.ok(notesEntry, 'NOTES.md should appear in existing_docs');
assert.strictEqual(notesEntry.has_gsd_marker, false, 'NOTES.md should not have GSD marker');
});
});
// ─── doc tooling detection ────────────────────────────────────────────────────
describe('doc tooling detection', () => {
let tmpDir;
beforeEach(() => {
tmpDir = createTempProject();
});
afterEach(() => {
cleanup(tmpDir);
});
test('detects Docusaurus config', () => {
fs.writeFileSync(path.join(tmpDir, 'docusaurus.config.js'), 'module.exports = {};', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.docusaurus, true);
});
test('detects VitePress config', () => {
fs.mkdirSync(path.join(tmpDir, '.vitepress'), { recursive: true });
fs.writeFileSync(path.join(tmpDir, '.vitepress', 'config.ts'), 'export default {};', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.vitepress, true);
});
test('detects MkDocs config', () => {
fs.writeFileSync(path.join(tmpDir, 'mkdocs.yml'), 'site_name: test', 'utf-8');
const result = runGsdTools(['docs-init'], tmpDir);
assert.ok(result.success, `Command failed: ${result.error}`);
const data = JSON.parse(result.output);
assert.strictEqual(data.doc_tooling.mkdocs, true);
});
});
// ────────────────────────────────────────────────────────────────────────
// Folded from tests/bug-3135-capture-backlog-workflow.test.cjs — consolidation epic #1969 (B3 #1972)
// ────────────────────────────────────────────────────────────────────────
{
const { describe: __foldDescribe } = require('node:test');
__foldDescribe("folded:bug-3135-capture-backlog-workflow (consolidation epic #1969 B3 #1972)", () => {
// allow-test-rule: source-text-is-the-product — workflow and command .md files (see #3135)
// ARE what the runtime loads; asserting their existence and behavioral content
// tests the deployed skill surface contract, not implementation internals.
'use strict';
// Regression tests for bug #3135.
//
// PR #2824 consolidated add-backlog into `gsd-capture --backlog` by creating
// a routing wrapper in commands/gsd/capture.md that delegates to
// workflows/add-backlog.md via execution_context. The workflow file was never
// created. Same gap class as reapply-patches.md (found and fixed in the same PR).
//
// Fix: create gsd-core/workflows/add-backlog.md with the full process
// ported from the deleted commands/gsd/add-backlog.md (git ref 87917131^).
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const ROOT = path.join(__dirname, '..');
const WORKFLOW = path.join(ROOT, 'gsd-core', 'workflows', 'add-backlog.md');
const COMMANDS_DIR = path.join(ROOT, 'commands', 'gsd');
// ─── #3135: add-backlog workflow ─────────────────────────────────────────────
describe('#3135: gsd-core/workflows/add-backlog.md', () => {
test('file exists', () => {
assert.ok(
fs.existsSync(WORKFLOW),
'gsd-core/workflows/add-backlog.md does not exist — capture --backlog has no implementation to load',
);
});
test('uses gsd-sdk query phase.next-decimal to find next 999.x slot', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('phase.next-decimal'),
'add-backlog.md must use gsd-sdk query phase.next-decimal to find the next 999.x number',
);
});
test('writes to ROADMAP.md', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(src.includes('ROADMAP.md'), 'add-backlog.md must write to ROADMAP.md');
});
test('creates a .planning/phases/ directory', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('.planning/phases') || src.includes('planning/phases'),
'add-backlog.md must create a phase directory under .planning/phases/',
);
});
test('uses generate-slug for the directory name', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('generate-slug'),
'add-backlog.md must use gsd-sdk query generate-slug to build the phase directory slug',
);
});
test('commits via gsd-sdk query commit', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('gsd-sdk query commit') || src.includes('query commit'),
'add-backlog.md must commit via gsd-sdk query commit',
);
});
test('writes ROADMAP entry before creating directory (#2280 ordering invariant)', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
const roadmapIdx = src.indexOf('ROADMAP.md');
const mkdirIdx = src.search(/mkdir|\.gitkeep/);
assert.ok(roadmapIdx !== -1, 'ROADMAP.md write step not found');
assert.ok(mkdirIdx !== -1, 'directory creation step not found');
assert.ok(
roadmapIdx < mkdirIdx,
'ROADMAP.md entry must be written BEFORE the phase directory is created (#2280 ordering invariant)',
);
});
test('uses 999.x numbering for backlog items', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('999'),
'add-backlog.md must document 999.x numbering scheme for backlog items',
);
});
test('documents /gsd-review-backlog for promotion', () => {
const src = fs.readFileSync(WORKFLOW, 'utf8');
assert.ok(
src.includes('review-backlog') || src.includes('gsd-review-backlog'),
'add-backlog.md should mention /gsd-review-backlog for promoting items to active milestone',
);
});
});
// ─── capture.md routing integrity ────────────────────────────────────────────
describe('#3135: capture.md correctly routes --backlog to add-backlog workflow', () => {
function executionContextIncludes(body) {
const blocks = [
...body.matchAll(/<execution_context(?:_extended)?>([\s\S]*?)<\/execution_context(?:_extended)?>/g),
].map((m) => m[1]);
const targets = [];
for (const blk of blocks) {
for (const line of blk.split('\n')) {
const t = line.trim();
if (!t.startsWith('@')) continue;
const rel = t.replace(/^@~?\/?(?:\.claude\/)?(?:gsd-core\/)?/, '');
targets.push(rel);
}
}
return targets;
}
test('capture.md execution_context @-includes add-backlog.md', () => {
const body = fs.readFileSync(path.join(COMMANDS_DIR, 'capture.md'), 'utf8');
const targets = executionContextIncludes(body);
assert.ok(
targets.some((t) => /(^|\/)workflows\/add-backlog\.md$/.test(t)),
`capture.md execution_context must @-include workflows/add-backlog.md; got: ${JSON.stringify(targets)}`,
);
});
});
});
}