- Extract execute-phase.md context_guard step prose to gsd-core/references/execute-phase-context-guard.md (@-ref lazy load), bringing execute-phase.md back under the ADR-857 phase-6 size ceiling (92914 < 93166 bytes) - Fix allow-test-rule comment: add `see #1452` per ADR-456 lint rule - Update feat-1452 tests to check the reference file for extracted content - Register execute-phase-context-guard.md in INVENTORY-MANIFEST.json and INVENTORY.md Workflow References section - Regenerate workflow-size-baseline.json after file shrinkage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
214 lines
8.6 KiB
JavaScript
214 lines
8.6 KiB
JavaScript
// allow-test-rule: source-text-is-the-product see #1452
|
|
// The execute-phase.md workflow and context-budget.md reference ARE the runtime
|
|
// contract loaded by AI runtimes. Asserting that the canonical wording for
|
|
// `workflow.context_guard_mode` is present in those files is the only way to
|
|
// verify runtimes will respect the flag at runtime.
|
|
|
|
/**
|
|
* Enhancement #1452: workflow.context_guard_mode
|
|
*
|
|
* Guards long execute-phase workflows from driving the host session to context
|
|
* exhaustion (ctx 100%). Before each wave, the orchestrator self-assesses
|
|
* context pressure using the degradation signals defined in context-budget.md.
|
|
*
|
|
* Modes:
|
|
* "warn" (default) — emit a structured warning + recommend /gsd:pause-work
|
|
* "auto" — auto-invoke pause-work before the next wave
|
|
* "off" — disable the guard entirely
|
|
*
|
|
* The check fires at wave boundaries ONLY (before spawning), never mid-wave.
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
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');
|
|
|
|
function readConfig(tmpDir) {
|
|
const configPath = path.join(tmpDir, '.planning', 'config.json');
|
|
return JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
}
|
|
|
|
const REPO_ROOT = path.join(__dirname, '..');
|
|
|
|
// ─── Schema registration ──────────────────────────────────────────────────────
|
|
|
|
describe('workflow.context_guard_mode in VALID_CONFIG_KEYS', () => {
|
|
test('is a recognized config key', () => {
|
|
const { VALID_CONFIG_KEYS } = require('../gsd-core/bin/lib/config.cjs');
|
|
assert.ok(
|
|
VALID_CONFIG_KEYS.has('workflow.context_guard_mode'),
|
|
'workflow.context_guard_mode should be in VALID_CONFIG_KEYS',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Default value ────────────────────────────────────────────────────────────
|
|
|
|
describe('workflow.context_guard_mode default value', () => {
|
|
let tmpDir;
|
|
beforeEach(() => { tmpDir = createTempProject(); });
|
|
afterEach(() => { cleanup(tmpDir); });
|
|
|
|
test('defaults to warn in new project config', () => {
|
|
const result = runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir });
|
|
assert.ok(result.success, `config-ensure-section failed: ${result.error}`);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(
|
|
config.workflow.context_guard_mode,
|
|
'warn',
|
|
'workflow.context_guard_mode should default to "warn" — proactive checkpoint warning without auto-pausing workflows',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── Round-trip ──────────────────────────────────────────────────────────────
|
|
|
|
describe('workflow.context_guard_mode config round-trip', () => {
|
|
let tmpDir;
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
runGsdTools('config-ensure-section', tmpDir, { HOME: tmpDir });
|
|
});
|
|
afterEach(() => { cleanup(tmpDir); });
|
|
|
|
test('config-set warn persists to config.json', () => {
|
|
const setResult = runGsdTools('config-set workflow.context_guard_mode warn', tmpDir);
|
|
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(config.workflow.context_guard_mode, 'warn');
|
|
});
|
|
|
|
test('config-set auto persists to config.json', () => {
|
|
const setResult = runGsdTools('config-set workflow.context_guard_mode auto', tmpDir);
|
|
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(config.workflow.context_guard_mode, 'auto');
|
|
});
|
|
|
|
test('config-set off persists to config.json', () => {
|
|
const setResult = runGsdTools('config-set workflow.context_guard_mode off', tmpDir);
|
|
assert.ok(setResult.success, `config-set failed: ${setResult.error}`);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(config.workflow.context_guard_mode, 'off');
|
|
});
|
|
|
|
test('persists in config.json as string', () => {
|
|
runGsdTools('config-set workflow.context_guard_mode warn', tmpDir);
|
|
|
|
const config = readConfig(tmpDir);
|
|
assert.strictEqual(config.workflow.context_guard_mode, 'warn');
|
|
assert.strictEqual(typeof config.workflow.context_guard_mode, 'string');
|
|
});
|
|
|
|
test('rejects unknown mode values with clear error', () => {
|
|
const result = runGsdTools('config-set workflow.context_guard_mode aggressive', tmpDir);
|
|
assert.strictEqual(result.success, false);
|
|
assert.match(result.error, /Invalid workflow\.context_guard_mode 'aggressive'/);
|
|
assert.match(result.error, /auto, warn, off/);
|
|
});
|
|
|
|
test('rejects partial match values', () => {
|
|
const result = runGsdTools('config-set workflow.context_guard_mode warnmode', tmpDir);
|
|
assert.strictEqual(result.success, false);
|
|
assert.match(result.error, /Invalid workflow\.context_guard_mode 'warnmode'/);
|
|
});
|
|
});
|
|
|
|
// ─── execute-phase contract ───────────────────────────────────────────────────
|
|
|
|
describe('execute-phase.md documents the context_guard step', () => {
|
|
let executePhase;
|
|
let contextGuardRef;
|
|
|
|
beforeEach(() => {
|
|
executePhase = fs.readFileSync(
|
|
path.join(REPO_ROOT, 'gsd-core', 'workflows', 'execute-phase.md'),
|
|
'utf-8',
|
|
);
|
|
// The step body is extracted to a reference file loaded via @-ref in execute-phase.md.
|
|
// Both files together constitute the execute-phase wave-boundary contract.
|
|
const refPath = path.join(REPO_ROOT, 'gsd-core', 'references', 'execute-phase-context-guard.md');
|
|
contextGuardRef = fs.existsSync(refPath) ? fs.readFileSync(refPath, 'utf-8') : '';
|
|
});
|
|
|
|
test('references workflow.context_guard_mode by canonical name', () => {
|
|
const combined = executePhase + '\n' + contextGuardRef;
|
|
assert.ok(
|
|
combined.includes('workflow.context_guard_mode'),
|
|
'execute-phase.md (or its @-referenced execute-phase-context-guard.md) must reference workflow.context_guard_mode so runtimes resolve the config-driven behavior',
|
|
);
|
|
});
|
|
|
|
test('defines context_guard step at wave boundaries', () => {
|
|
assert.ok(
|
|
executePhase.includes('context_guard') || executePhase.includes('context-guard'),
|
|
'execute-phase.md must define a context_guard step (or @-ref to it) that fires before each wave',
|
|
);
|
|
});
|
|
|
|
test('references context-budget.md tiers in the guard step', () => {
|
|
const combined = executePhase + '\n' + contextGuardRef;
|
|
assert.ok(
|
|
combined.includes('context-budget') || combined.includes('POOR') || combined.includes('DEGRADING'),
|
|
'execute-phase.md context_guard (or its @-referenced file) must reference context-budget.md degradation tiers',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── context-budget.md contract ──────────────────────────────────────────────
|
|
|
|
describe('context-budget.md documents POOR-tier trigger action', () => {
|
|
let contextBudget;
|
|
|
|
beforeEach(() => {
|
|
contextBudget = fs.readFileSync(
|
|
path.join(REPO_ROOT, 'gsd-core', 'references', 'context-budget.md'),
|
|
'utf-8',
|
|
);
|
|
});
|
|
|
|
test('defines POOR tier', () => {
|
|
assert.ok(
|
|
contextBudget.includes('POOR'),
|
|
'context-budget.md must define the POOR tier',
|
|
);
|
|
});
|
|
|
|
test('connects POOR tier to pause-work', () => {
|
|
assert.ok(
|
|
contextBudget.includes('pause-work') || contextBudget.includes('pause_work'),
|
|
'context-budget.md POOR-tier rule must reference pause-work as the trigger action',
|
|
);
|
|
});
|
|
|
|
test('documents context_guard_mode values', () => {
|
|
assert.ok(
|
|
contextBudget.includes('context_guard_mode'),
|
|
'context-budget.md must document the workflow.context_guard_mode config key',
|
|
);
|
|
});
|
|
});
|
|
|
|
// ─── planning-config.md reference parity ─────────────────────────────────────
|
|
|
|
describe('planning-config.md documents workflow.context_guard_mode', () => {
|
|
test('includes the key in the reference table', () => {
|
|
const planningConfig = fs.readFileSync(
|
|
path.join(REPO_ROOT, 'gsd-core', 'references', 'planning-config.md'),
|
|
'utf-8',
|
|
);
|
|
assert.ok(
|
|
planningConfig.includes('workflow.context_guard_mode'),
|
|
'planning-config.md reference must include workflow.context_guard_mode so users know the config knob exists',
|
|
);
|
|
});
|
|
});
|