refactor(#3309): add config-validation health-diagnostic rules
W003, E005, W004, W008, W016, W012, W013, W014, W015, W022 — config.json existence, parseability, and field-validity checks, migrated onto the frozen rule table per ADR-3180 §8.2.
This commit is contained in:
296
src/health-diagnostic-rules/config-validation.cts
Normal file
296
src/health-diagnostic-rules/config-validation.cts
Normal file
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Health Diagnostic — config.json validation rules (Phase 11, #3309,
|
||||
* ADR-3180 §8.2/§8.3/§8.5).
|
||||
*
|
||||
* Group: "config.json validation" (design doc, "Rule table organization"
|
||||
* table) — W003, W004, W022 (one rule, three internal conditions), E005,
|
||||
* W008, W016, W012, W013, W014, W015.
|
||||
*
|
||||
* Ported behavior-preserving from `cmdValidateHealth`'s config.json blocks
|
||||
* (`src/verify.cts:1777-1835` for W003/W004/W022/E005,
|
||||
* `src/verify.cts:1837-1865` for W008/W016,
|
||||
* `src/verify.cts:2136-2191` for W012/W013/W014/W015). Every rule here reads
|
||||
* ONLY `snapshot.config` (`{value, scope, exists}`, `src/planning-snapshot.cts`'s
|
||||
* `buildConfigField`).
|
||||
*
|
||||
* W022 stays a SINGLE code across its three call sites per the design doc's
|
||||
* "Rejected alternatives" §3: all three are variations on one question ("is
|
||||
* `models` well-formed"), not a genuine multi-subject conflation. This rule's
|
||||
* `checkW022` mirrors the original's exact if / else-if control flow
|
||||
* (`verify.cts:1799-1824`): the object-shaped branch loops every `models`
|
||||
* entry (0-N diagnostics, one per malformed entry); the non-object branch
|
||||
* fires independently and ONLY when the object-shaped branch did not run —
|
||||
* `models` is never checked against both.
|
||||
*
|
||||
* Two disclosed fidelity reductions, forced by `snapshot.config`'s shape
|
||||
* (neither is available without violating §8.1 rule 1's "no ambient I/O in a
|
||||
* rule's `check`"):
|
||||
*
|
||||
* - E005's original message interpolates the live `JSON.parse` error text
|
||||
* (`config.json: JSON parse error - ${err.message}`, `verify.cts:1829`).
|
||||
* `buildConfigField` (`src/planning-snapshot.cts:268-281`) catches and
|
||||
* discards that error, collapsing an unparseable config.json to
|
||||
* `{value: null, scope: UNREADABLE, exists: true}` with no error text
|
||||
* anywhere in the snapshot. This rule's message drops the interpolated
|
||||
* suffix rather than fabricate error text the snapshot never carried.
|
||||
* - W016's original message interpolates `${slash('ai-integration-phase')}`
|
||||
* (`verify.cts:1856`), a per-project runtime-resolved value
|
||||
* (`formatGsdSlash`, `src/runtime-slash.cts`) this rule's `(snapshot) =>
|
||||
* Diagnostic[]` signature has no access to. Hardcodes the canonical
|
||||
* `/gsd-ai-integration-phase` hyphen form instead, mirroring the sibling
|
||||
* "Phase directory structure" group's W009 rule
|
||||
* (`src/health-diagnostic-rules/phase-structure.cts`), which hardcodes
|
||||
* `/gsd-plan-phase` the same way for the identical reason.
|
||||
*
|
||||
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
|
||||
*
|
||||
* ADR-457 build-at-publish: source in
|
||||
* src/health-diagnostic-rules/config-validation.cts, compiled to
|
||||
* gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs (gitignored).
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- type-only; erased at compile time, no runtime require emitted
|
||||
import type planningSnapshotMod = require('../planning-snapshot.cjs');
|
||||
|
||||
type PlanningSnapshot = ReturnType<typeof planningSnapshotMod.buildPlanningSnapshot>;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports -- health-diagnostic.cjs is an export= CommonJS module
|
||||
import healthDiagnosticMod = require('../health-diagnostic.cjs');
|
||||
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = healthDiagnosticMod;
|
||||
type Diagnostic = healthDiagnosticMod.Diagnostic;
|
||||
type Remedy = healthDiagnosticMod.Remedy;
|
||||
type Rule = healthDiagnosticMod.Rule;
|
||||
|
||||
import { VALID_PROFILES, VALID_TIERS, VALID_PHASE_TYPES } from '../model-catalog.cjs';
|
||||
|
||||
// verify.cts:2141 — inlined literal, not exported from anywhere; same list.
|
||||
const VALID_BRANCHING_STRATEGIES = ['none', 'phase', 'milestone'];
|
||||
|
||||
function adviseRemedy(command: string): Remedy {
|
||||
return { action: REMEDY_ACTION.ADVISE, risk: REMEDY_RISK.NONE, args: { command } };
|
||||
}
|
||||
|
||||
// ─── W003 — config.json not found (verify.cts:1777-1785) ───────────────────
|
||||
|
||||
function checkW003(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
if (snapshot.config.exists) return [];
|
||||
return [
|
||||
{
|
||||
code: 'W003',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json not found',
|
||||
remedy: { action: REMEDY_ACTION.CREATE_CONFIG, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── E005 — config.json JSON parse error (verify.cts:1825-1834) ────────────
|
||||
//
|
||||
// `exists: true, value: null` is exactly `buildConfigField`'s "present but
|
||||
// unparseable" contract (planning-snapshot.cts:268-281) — the same
|
||||
// discriminator that separates this from W003's "absent" case.
|
||||
|
||||
function checkE005(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
if (!snapshot.config.exists || snapshot.config.value !== null) return [];
|
||||
return [
|
||||
{
|
||||
code: 'E005',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'config.json: JSON parse error',
|
||||
remedy: { action: REMEDY_ACTION.RESET_CONFIG, risk: REMEDY_RISK.DESTRUCTIVE, args: {} },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// ─── W004 — invalid model_profile (verify.cts:1790-1797) ───────────────────
|
||||
|
||||
function checkW004(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const profile = value['model_profile'];
|
||||
if (profile && !VALID_PROFILES.includes(profile as string)) {
|
||||
return [
|
||||
{
|
||||
code: 'W004',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: invalid model_profile "${profile as string}"`,
|
||||
remedy: adviseRemedy(`Valid values: ${VALID_PROFILES.join(', ')}`),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W008 — workflow.nyquist_validation absent (verify.cts:1841-1851) ──────
|
||||
|
||||
function checkW008(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
const workflow = value ? (value['workflow'] as Record<string, unknown> | undefined) : undefined;
|
||||
if (workflow && workflow['nyquist_validation'] === undefined) {
|
||||
return [
|
||||
{
|
||||
code: 'W008',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)',
|
||||
remedy: { action: REMEDY_ACTION.ADD_NYQUIST_KEY, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W016 — workflow.ai_integration_phase absent (verify.cts:1852-1861) ────
|
||||
|
||||
function checkW016(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
const workflow = value ? (value['workflow'] as Record<string, unknown> | undefined) : undefined;
|
||||
if (workflow && workflow['ai_integration_phase'] === undefined) {
|
||||
return [
|
||||
{
|
||||
code: 'W016',
|
||||
severity: SEVERITY.WARNING,
|
||||
message:
|
||||
'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /gsd-ai-integration-phase before planning AI system phases)',
|
||||
remedy: { action: REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W012 — invalid branching_strategy (verify.cts:2141-2152) ──────────────
|
||||
|
||||
function checkW012(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const strategy = value['branching_strategy'];
|
||||
if (strategy && !VALID_BRANCHING_STRATEGIES.includes(strategy as string)) {
|
||||
return [
|
||||
{
|
||||
code: 'W012',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: invalid branching_strategy "${strategy as string}"`,
|
||||
remedy: adviseRemedy(`Valid values: ${VALID_BRANCHING_STRATEGIES.join(', ')}`),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W013 — context_window not a positive integer (verify.cts:2154-2164) ───
|
||||
|
||||
function checkW013(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const cw = value['context_window'];
|
||||
if (cw !== undefined && (typeof cw !== 'number' || cw <= 0 || !Number.isInteger(cw))) {
|
||||
return [
|
||||
{
|
||||
code: 'W013',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: context_window should be a positive integer, got "${cw as string}"`,
|
||||
remedy: adviseRemedy('Set to 200000 (default) or 1000000 (for 1M models)'),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W014 — phase_branch_template missing {phase} (verify.cts:2166-2176) ───
|
||||
|
||||
function checkW014(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const tmpl = value['phase_branch_template'];
|
||||
if (tmpl && !(tmpl as string).includes('{phase}')) {
|
||||
return [
|
||||
{
|
||||
code: 'W014',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: phase_branch_template missing {phase} placeholder',
|
||||
remedy: adviseRemedy('Template must include {phase} for phase number substitution'),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W015 — milestone_branch_template missing {milestone} (verify.cts:2177-2187) ─
|
||||
|
||||
function checkW015(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const tmpl = value['milestone_branch_template'];
|
||||
if (tmpl && !(tmpl as string).includes('{milestone}')) {
|
||||
return [
|
||||
{
|
||||
code: 'W015',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: milestone_branch_template missing {milestone} placeholder',
|
||||
remedy: adviseRemedy('Template must include {milestone} for version substitution'),
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
// ─── W022 — models malformed, 3 internal conditions (verify.cts:1798-1824) ─
|
||||
//
|
||||
// Mirrors the original if / else-if chain exactly: the object-shaped branch
|
||||
// (a: unknown phase type, b: invalid tier value) loops every `models` entry,
|
||||
// pushing 0-N diagnostics; the non-object branch (c) is an ELSE-IF, so it
|
||||
// only runs when `models` is truthy but did NOT satisfy "object, not array" —
|
||||
// (a)/(b) are never evaluated against a non-object `models`.
|
||||
|
||||
function checkW022(snapshot: PlanningSnapshot): Diagnostic[] {
|
||||
const value = snapshot.config.value;
|
||||
if (!value) return [];
|
||||
const diagnostics: Diagnostic[] = [];
|
||||
const configModels = value['models'];
|
||||
|
||||
if (configModels && typeof configModels === 'object' && !Array.isArray(configModels)) {
|
||||
for (const [phaseType, tierValue] of Object.entries(configModels as Record<string, unknown>)) {
|
||||
if (!VALID_PHASE_TYPES.has(phaseType)) {
|
||||
diagnostics.push({
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: models has an unknown phase type "${phaseType}" which will be ignored`,
|
||||
remedy: adviseRemedy(`Valid phase types: ${[...VALID_PHASE_TYPES].join(', ')}`),
|
||||
});
|
||||
} else if (typeof tierValue !== 'string' || !VALID_TIERS.has(tierValue)) {
|
||||
diagnostics.push({
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: models.${phaseType} has an invalid tier value ${JSON.stringify(tierValue)} which will be ignored`,
|
||||
remedy: adviseRemedy(`Valid tiers: ${[...VALID_TIERS].join(', ')}`),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (configModels !== undefined && configModels !== null) {
|
||||
diagnostics.push({
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: `config.json: models is set to ${JSON.stringify(configModels)}, but must be an object mapping phase types to tiers — this value will be ignored`,
|
||||
remedy: adviseRemedy('Set models to an object like {"planning": "sonnet"}, or remove the key to use profile defaults'),
|
||||
});
|
||||
}
|
||||
|
||||
return diagnostics;
|
||||
}
|
||||
|
||||
// ─── Exports ────────────────────────────────────────────────────────────────
|
||||
|
||||
const RULES: Rule[] = [
|
||||
{ code: 'W003', severity: SEVERITY.WARNING, check: checkW003 },
|
||||
{ code: 'E005', severity: SEVERITY.ERROR, check: checkE005 },
|
||||
{ code: 'W004', severity: SEVERITY.WARNING, check: checkW004 },
|
||||
{ code: 'W008', severity: SEVERITY.WARNING, check: checkW008 },
|
||||
{ code: 'W016', severity: SEVERITY.WARNING, check: checkW016 },
|
||||
{ code: 'W012', severity: SEVERITY.WARNING, check: checkW012 },
|
||||
{ code: 'W013', severity: SEVERITY.WARNING, check: checkW013 },
|
||||
{ code: 'W014', severity: SEVERITY.WARNING, check: checkW014 },
|
||||
{ code: 'W015', severity: SEVERITY.WARNING, check: checkW015 },
|
||||
{ code: 'W022', severity: SEVERITY.WARNING, check: checkW022 },
|
||||
];
|
||||
|
||||
export = { RULES };
|
||||
699
tests/health-diagnostic-rules/config-validation.test.cjs
Normal file
699
tests/health-diagnostic-rules/config-validation.test.cjs
Normal file
@@ -0,0 +1,699 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Tests for `src/health-diagnostic-rules/config-validation.cts` (Phase 11,
|
||||
* #3309, ADR-3180 §8.2/§8.3/§8.5) — group "config.json validation": W003,
|
||||
* E005, W004, W008, W016, W012, W013, W014, W015, W022.
|
||||
*
|
||||
* Design: .gsd/phase/refactor-3309-health-diagnostic-rule-table/40-design.md
|
||||
* Test matrix: .gsd/phase/refactor-3309-health-diagnostic-rule-table/50-test-matrix.md
|
||||
*
|
||||
* Fixture provenance (§8.5 + CONTRIBUTING "Fixture provenance (#2371)"):
|
||||
*
|
||||
* - W003, W008, W016 are STRUCTURAL ABSENCE (a file, or a key, is missing) —
|
||||
* exempt from external-citation provenance, mirrors
|
||||
* tests/health-diagnostic-rules/root-existence.test.cjs's own framing.
|
||||
* - E005, W004, W012, W013, W014, W015, W022 are MECHANICAL MUTATION: each
|
||||
* fixture starts from `gsd-core/templates/config.json` (the real shipped
|
||||
* shape, parsed once as `BASE_CONFIG`) with exactly ONE field changed to
|
||||
* the invalid value under test, per the design doc's Fixture provenance
|
||||
* §4. `w022`'s `models` key and `branching_strategy`/`context_window`/
|
||||
* `phase_branch_template`/`milestone_branch_template` are not present in
|
||||
* the shipped default at all (they are optional, additive keys) — for
|
||||
* those the "one field changed" mutation is adding exactly that one key
|
||||
* with its invalid value, the generic-mutation equivalent when there is no
|
||||
* existing value to corrupt.
|
||||
*
|
||||
* Every fixture is driven through the REAL `buildPlanningSnapshot(cwd)`
|
||||
* against a REAL temp `.planning/` directory with a REAL config.json file
|
||||
* written to disk — mirrors tests/planning-snapshot.test.cjs's own
|
||||
* `writeConfig` helper exactly (same shape, same call site convention).
|
||||
*
|
||||
* TDD RED: `src/health-diagnostic-rules/config-validation.cts` does not
|
||||
* exist yet at the start of this batch — this file's
|
||||
* `require('../../gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs')`
|
||||
* throws MODULE_NOT_FOUND until this batch's implementation lands.
|
||||
*/
|
||||
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { createTempDir, cleanup } = require('../helpers.cjs');
|
||||
|
||||
const configValidation = require('../../gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs');
|
||||
const { RULES } = configValidation;
|
||||
|
||||
const { buildPlanningSnapshot } = require('../../gsd-core/bin/lib/planning-snapshot.cjs');
|
||||
const { SEVERITY, REMEDY_ACTION, REMEDY_RISK } = require('../../gsd-core/bin/lib/health-diagnostic.cjs');
|
||||
|
||||
// Real shipped default, parsed once — the mutation base for every
|
||||
// MECHANICAL MUTATION fixture below (design doc Fixture provenance §4).
|
||||
const TEMPLATE_CONFIG_PATH = path.join(__dirname, '..', '..', 'gsd-core', 'templates', 'config.json');
|
||||
const BASE_CONFIG = JSON.parse(fs.readFileSync(TEMPLATE_CONFIG_PATH, 'utf-8'));
|
||||
|
||||
function planningDirOf(cwd) {
|
||||
return path.join(cwd, '.planning');
|
||||
}
|
||||
|
||||
function writeConfig(cwd, obj) {
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDirOf(cwd), 'config.json'), JSON.stringify(obj));
|
||||
}
|
||||
|
||||
function writeRawConfig(cwd, rawText) {
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
fs.writeFileSync(path.join(planningDirOf(cwd), 'config.json'), rawText);
|
||||
}
|
||||
|
||||
// Deep-clones BASE_CONFIG and applies exactly one mutation, mirroring the
|
||||
// design doc's "mechanical, rule-blind mutation of the real shipped
|
||||
// template" fixture class.
|
||||
function mutatedConfig(mutate) {
|
||||
const clone = JSON.parse(JSON.stringify(BASE_CONFIG));
|
||||
mutate(clone);
|
||||
return clone;
|
||||
}
|
||||
|
||||
function ruleFor(code) {
|
||||
const rule = RULES.find((r) => r.code === code);
|
||||
assert.ok(rule, `rule ${code} not found in RULES`);
|
||||
return rule;
|
||||
}
|
||||
|
||||
// ─── RULES shape ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('RULES (config-validation group)', () => {
|
||||
test('exports exactly 10 rules: W003, E005, W004, W008, W016, W012, W013, W014, W015, W022', () => {
|
||||
assert.deepEqual(
|
||||
RULES.map((r) => r.code).sort(),
|
||||
['E005', 'W003', 'W004', 'W008', 'W012', 'W013', 'W014', 'W015', 'W016', 'W022'].sort(),
|
||||
);
|
||||
});
|
||||
|
||||
test('E005 is severity ERROR; the rest are severity WARNING', () => {
|
||||
assert.equal(ruleFor('E005').severity, SEVERITY.ERROR);
|
||||
for (const code of ['W003', 'W004', 'W008', 'W012', 'W013', 'W014', 'W015', 'W016', 'W022']) {
|
||||
assert.equal(ruleFor(code).severity, SEVERITY.WARNING, `${code} should be WARNING`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W003 — config.json not found ───────────────────────────────────────────
|
||||
|
||||
describe('W003 — config.json not found', () => {
|
||||
test('fires when config.json is absent (structural absence)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w003-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W003').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W003',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json not found',
|
||||
remedy: { action: REMEDY_ACTION.CREATE_CONFIG, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when config.json exists', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w003-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W003').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── E005 — config.json JSON parse error ────────────────────────────────────
|
||||
|
||||
describe('E005 — config.json invalid JSON', () => {
|
||||
test('fires when config.json exists but is unparseable', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e005-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeRawConfig(cwd, '{ not valid json');
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('E005').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'E005',
|
||||
severity: SEVERITY.ERROR,
|
||||
message: 'config.json: JSON parse error',
|
||||
remedy: { action: REMEDY_ACTION.RESET_CONFIG, risk: REMEDY_RISK.DESTRUCTIVE, args: {} },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when config.json is absent (that is W003s job)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e005-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('E005').check(snapshot), []);
|
||||
assert.equal(ruleFor('W003').check(snapshot).length, 1);
|
||||
});
|
||||
|
||||
test('does not fire when config.json is well-formed', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-e005-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('E005').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W004 — invalid model_profile ───────────────────────────────────────────
|
||||
|
||||
describe('W004 — invalid model_profile', () => {
|
||||
test('fires on an invalid model_profile value (mutated shipped config)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w004-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.model_profile = 'not-a-real-profile';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W004').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W004',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: invalid model_profile "not-a-real-profile"',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Valid values: quality, balanced, budget, adaptive, inherit' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire on a valid model_profile', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w004-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.model_profile = 'balanced';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W004').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when model_profile is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w004-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W004').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W008 — workflow.nyquist_validation absent ──────────────────────────────
|
||||
|
||||
describe('W008 — workflow.nyquist_validation absent', () => {
|
||||
test('fires when workflow is present but nyquist_validation key is deleted', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w008-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
delete c.workflow.nyquist_validation;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W008').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W008',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: workflow.nyquist_validation absent (defaults to enabled but agents may skip)',
|
||||
remedy: { action: REMEDY_ACTION.ADD_NYQUIST_KEY, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when nyquist_validation is present (shipped default)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w008-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W008').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when workflow itself is absent (guard mirrors original)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w008-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
delete c.workflow;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W008').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W016 — workflow.ai_integration_phase absent ────────────────────────────
|
||||
|
||||
describe('W016 — workflow.ai_integration_phase absent', () => {
|
||||
test('fires on the unmodified shipped default — ai_integration_phase is not in the template at all (structural absence)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w016-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W016').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W016',
|
||||
severity: SEVERITY.WARNING,
|
||||
message:
|
||||
'config.json: workflow.ai_integration_phase absent (defaults to enabled — run /gsd-ai-integration-phase before planning AI system phases)',
|
||||
remedy: { action: REMEDY_ACTION.ADD_AI_INTEGRATION_PHASE_KEY, risk: REMEDY_RISK.NONE, args: {} },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when ai_integration_phase key is present', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w016-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.workflow.ai_integration_phase = true;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W016').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when workflow itself is absent (guard mirrors original)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w016-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
delete c.workflow;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W016').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W012 — invalid branching_strategy ──────────────────────────────────────
|
||||
|
||||
describe('W012 — invalid branching_strategy', () => {
|
||||
test('fires on an invalid branching_strategy value', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w012-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.branching_strategy = 'bogus-strategy';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W012').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W012',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: invalid branching_strategy "bogus-strategy"',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Valid values: none, phase, milestone' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
for (const valid of ['none', 'phase', 'milestone']) {
|
||||
test(`does not fire on valid branching_strategy "${valid}"`, (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w012-valid-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.branching_strategy = valid;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W012').check(snapshot), []);
|
||||
});
|
||||
}
|
||||
|
||||
test('does not fire when branching_strategy is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w012-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W012').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W013 — context_window not a positive integer ───────────────────────────
|
||||
|
||||
describe('W013 — context_window not a positive integer', () => {
|
||||
test('fires on a non-integer context_window', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.context_window = 3.5;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W013').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W013',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: context_window should be a positive integer, got "3.5"',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Set to 200000 (default) or 1000000 (for 1M models)' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('fires on a non-positive context_window (boundary: 0)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.context_window = 0;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.equal(ruleFor('W013').check(snapshot).length, 1);
|
||||
});
|
||||
|
||||
test('fires on a negative context_window', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.context_window = -1;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.equal(ruleFor('W013').check(snapshot).length, 1);
|
||||
});
|
||||
|
||||
test('does not fire on a valid positive integer (boundary: 1)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-4-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.context_window = 1;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W013').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire on the conventional default (200000)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-5-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.context_window = 200000;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W013').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when context_window is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w013-6-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W013').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W014 — phase_branch_template missing {phase} ───────────────────────────
|
||||
|
||||
describe('W014 — phase_branch_template missing {phase} placeholder', () => {
|
||||
test('fires when the placeholder is stripped', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w014-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.phase_branch_template = 'phase/no-placeholder-here';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W014').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W014',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: phase_branch_template missing {phase} placeholder',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Template must include {phase} for phase number substitution' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when the placeholder is present', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w014-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.phase_branch_template = 'phase/{phase}';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W014').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when phase_branch_template is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w014-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W014').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W015 — milestone_branch_template missing {milestone} ──────────────────
|
||||
|
||||
describe('W015 — milestone_branch_template missing {milestone} placeholder', () => {
|
||||
test('fires when the placeholder is stripped', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w015-1-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.milestone_branch_template = 'milestone/no-placeholder-here';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W015').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W015',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: milestone_branch_template missing {milestone} placeholder',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Template must include {milestone} for version substitution' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not fire when the placeholder is present', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w015-2-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.milestone_branch_template = 'milestone/{milestone}';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W015').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when milestone_branch_template is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w015-3-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W015').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── W022 — models malformed (3 internal conditions, 1 code) ───────────────
|
||||
|
||||
describe('W022 — config.json models malformed', () => {
|
||||
test('(a) unknown phase type key fires', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022a-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = { not_a_real_phase_type: 'sonnet' };
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W022').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: models has an unknown phase type "not_a_real_phase_type" which will be ignored',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: {
|
||||
command: 'Valid phase types: planning, discuss, research, execution, verification, completion',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('(b) known phase type with an invalid tier value fires', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022b-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = { planning: 'not-a-real-tier' };
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W022').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message: 'config.json: models.planning has an invalid tier value "not-a-real-tier" which will be ignored',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: { command: 'Valid tiers: opus, sonnet, haiku, inherit' },
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('(c) models present but not an object fires, and does NOT also run (a)/(b)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022c-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = 'not-an-object';
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W022').check(snapshot);
|
||||
|
||||
assert.deepEqual(diagnostics, [
|
||||
{
|
||||
code: 'W022',
|
||||
severity: SEVERITY.WARNING,
|
||||
message:
|
||||
'config.json: models is set to "not-an-object", but must be an object mapping phase types to tiers — this value will be ignored',
|
||||
remedy: {
|
||||
action: REMEDY_ACTION.ADVISE,
|
||||
risk: REMEDY_RISK.NONE,
|
||||
args: {
|
||||
command: 'Set models to an object like {"planning": "sonnet"}, or remove the key to use profile defaults',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('(c) an array also counts as "not an object" (Array.isArray guard)', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022c-array-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = ['sonnet'];
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W022').check(snapshot);
|
||||
assert.equal(diagnostics.length, 1);
|
||||
assert.equal(diagnostics[0].code, 'W022');
|
||||
});
|
||||
|
||||
test('multiple malformed entries in one models object each produce their own diagnostic', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022-multi-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = { planning: 'bogus-tier', not_a_real_phase_type: 'sonnet' };
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
const diagnostics = ruleFor('W022').check(snapshot);
|
||||
assert.equal(diagnostics.length, 2);
|
||||
for (const d of diagnostics) assert.equal(d.code, 'W022');
|
||||
});
|
||||
|
||||
test('does not fire when models is a well-formed object', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022-ok-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = { planning: 'sonnet', research: 'inherit' };
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W022').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when models is absent', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022-absent-');
|
||||
t.after(() => cleanup(cwd));
|
||||
writeConfig(cwd, BASE_CONFIG);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W022').check(snapshot), []);
|
||||
});
|
||||
|
||||
test('does not fire when models is null', (t) => {
|
||||
const cwd = createTempDir('gsd-3309-w022-null-');
|
||||
t.after(() => cleanup(cwd));
|
||||
const cfg = mutatedConfig((c) => {
|
||||
c.models = null;
|
||||
});
|
||||
writeConfig(cwd, cfg);
|
||||
|
||||
const snapshot = buildPlanningSnapshot(cwd);
|
||||
assert.deepEqual(ruleFor('W022').check(snapshot), []);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user