Files
get-shit-done/tests/enh-2430-learnings-consumption.test.cjs
Tom Boucher b432d4a726 feat(workflows): close LEARNINGS.md consumption-and-graduation loop (#2490)
* fix(tests): update 5 source-text tests to read config-schema.cjs

VALID_CONFIG_KEYS moved from config.cjs to config-schema.cjs in the
drift-prevention companion PR. Tests that read config.cjs source text
and checked for key literal includes() now point to the correct file.

Closes #2480

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(workflows): close LEARNINGS.md consumption-and-graduation loop (#2430)

Part A — Consumption: extend plan-phase.md cross-phase context load to include
LEARNINGS.md files from the 3 most recent prior phases (same recency gate as
CONTEXT.md + SUMMARY.md: CONTEXT_WINDOW >= 500000 only). Also loads LEARNINGS.md
from any phases in the Depends-on chain. Silent skip if absent; 15% context
budget cap with oldest-first truncation; [from Phase N LEARNINGS] attribution.

Part B — Graduation: add graduation_scan step to transition.md (after
evolve_project) that delegates to new graduation.md helper workflow. The helper
clusters recurring items across the last N phases (default window=5, threshold=3)
using Jaccard lexical similarity, surfaces HITL Promote/Defer/Dismiss prompts,
routes promotions to PROJECT.md or PATTERNS.md by category, annotates graduated
items with `graduated:` field, and persists dismissed/deferred clusters in
STATE.md graduation_backlog. Always non-blocking; silently no-ops on first phase
or when data is insufficient.

Also: adds optional `graduated:` annotation docs to extract_learnings.md schema.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(graduation): address CodeRabbit review findings on PR #2490

- graduation.md: unify insufficient-data guard to silent-skip (remove
  contradictory [no-op] print path)
- graduation.md: add TEXT_MODE fallback for HITL cluster prompts
- graduation.md: add A (defer-all) to accepted actions [P/D/X/A]
- graduation.md: tag untyped code fences with text language (MD040)
- transition.md: tag untyped graduation.md fence with text language

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(graduation): rephrase TEXT_MODE line to avoid prompt-injection scanner false positive

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-20 18:21:35 -04:00

235 lines
8.6 KiB
JavaScript

'use strict';
/**
* Tests for #2430 — LEARNINGS.md consumption loop.
*
* Part A: plan-phase.md cross-phase context load includes LEARNINGS.md
* Part B: transition.md graduation_scan step + graduation.md helper
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const WORKFLOWS_DIR = path.join(__dirname, '../get-shit-done/workflows');
function readWorkflow(name) {
return fs.readFileSync(path.join(WORKFLOWS_DIR, name), 'utf-8');
}
describe('enh-2430 Part A — plan-phase LEARNINGS.md context load', () => {
let content;
test('plan-phase.md includes LEARNINGS.md in cross-phase context load', () => {
content = readWorkflow('plan-phase.md');
assert.ok(
content.includes('LEARNINGS.md files from the 3 most recent completed phases'),
'plan-phase.md must mention LEARNINGS.md in cross-phase context block'
);
});
test('plan-phase.md LEARNINGS load is inside the 1M context-window gate', () => {
content = content || readWorkflow('plan-phase.md');
const windowBlock = content.match(/\$\{CONTEXT_WINDOW >= 500000[\s\S]*?\` : ''\}/);
assert.ok(windowBlock, 'CONTEXT_WINDOW gate block must exist');
assert.ok(
windowBlock[0].includes('LEARNINGS.md'),
'LEARNINGS.md load must be inside the CONTEXT_WINDOW >= 500000 gate'
);
});
test('plan-phase.md source attribution mentioned for LEARNINGS load', () => {
content = content || readWorkflow('plan-phase.md');
assert.ok(
content.includes('[from Phase N LEARNINGS]') || content.includes('source attribution'),
'plan-phase.md must document source attribution for loaded LEARNINGS.md content'
);
});
test('plan-phase.md handles missing LEARNINGS.md gracefully (silent skip)', () => {
content = content || readWorkflow('plan-phase.md');
assert.ok(
content.includes('skip silently if a phase has no LEARNINGS.md') ||
content.includes('skip silently'),
'plan-phase.md must document silent skip when LEARNINGS.md is absent'
);
});
test('plan-phase.md LEARNINGS load includes Depends-on chain', () => {
content = content || readWorkflow('plan-phase.md');
const dependsSection = content.match(/Depends on.*?(\n.*?)+/);
assert.ok(
content.includes('LEARNINGS.md from any phases listed in'),
'plan-phase.md must load LEARNINGS.md for Depends on chain phases'
);
});
test('plan-phase.md specifies context budget limit for LEARNINGS', () => {
content = content || readWorkflow('plan-phase.md');
assert.ok(
content.includes('15%') || content.includes('drop oldest'),
'plan-phase.md must specify budget limit and truncation strategy for LEARNINGS'
);
});
});
describe('enh-2430 Part B — graduation_scan in transition.md', () => {
let content;
test('transition.md contains graduation_scan step', () => {
content = readWorkflow('transition.md');
assert.ok(
content.includes('graduation_scan'),
'transition.md must contain graduation_scan step'
);
});
test('graduation_scan is placed after evolve_project step', () => {
content = content || readWorkflow('transition.md');
const evolvePos = content.indexOf('name="evolve_project"');
const graduationPos = content.indexOf('name="graduation_scan"');
assert.ok(evolvePos >= 0, 'evolve_project step must exist');
assert.ok(graduationPos >= 0, 'graduation_scan step must exist');
assert.ok(
graduationPos > evolvePos,
'graduation_scan must appear after evolve_project in transition.md'
);
});
test('graduation_scan is non-blocking (transition continues regardless)', () => {
content = content || readWorkflow('transition.md');
const scanBlock = content.match(/name="graduation_scan"[\s\S]*?<\/step>/);
assert.ok(scanBlock, 'graduation_scan step must be parseable');
assert.ok(
scanBlock[0].includes('non-blocking') || scanBlock[0].includes('always non-blocking'),
'graduation_scan must be documented as non-blocking'
);
});
test('graduation_scan delegates to graduation.md helper', () => {
content = content || readWorkflow('transition.md');
assert.ok(
content.includes('graduation.md'),
'graduation_scan must reference graduation.md helper workflow'
);
});
});
describe('enh-2430 Part B — graduation.md helper workflow', () => {
let content;
test('graduation.md exists', () => {
content = readWorkflow('graduation.md');
assert.ok(content.length > 0, 'graduation.md must exist and be non-empty');
});
test('graduation.md documents features.graduation config flag', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('features.graduation'),
'graduation.md must document features.graduation config flag'
);
});
test('graduation.md documents graduation_window config', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('graduation_window'),
'graduation.md must document features.graduation_window config'
);
});
test('graduation.md documents graduation_threshold config', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('graduation_threshold'),
'graduation.md must document features.graduation_threshold config'
);
});
test('graduation.md specifies HITL: Promote / Defer / Dismiss', () => {
content = content || readWorkflow('graduation.md');
assert.ok(content.includes('Promote'), 'graduation.md must document Promote action');
assert.ok(content.includes('Defer'), 'graduation.md must document Defer action');
assert.ok(content.includes('Dismiss'), 'graduation.md must document Dismiss action');
});
test('graduation.md specifies category→target routing', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('PROJECT.md') && content.includes('PATTERNS.md'),
'graduation.md must route categories to appropriate target files'
);
});
test('graduation.md specifies graduation_backlog in STATE.md', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('graduation_backlog'),
'graduation.md must document STATE.md graduation_backlog for Defer/Dismiss'
);
});
test('graduation.md skips items with graduated: annotation', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('graduated:') || content.includes('Graduated:'),
'graduation.md must skip already-graduated items'
);
});
test('graduation.md has silent no-op for first phase / insufficient data', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('no-op') || content.includes('silent'),
'graduation.md must silently no-op when there is insufficient data'
);
});
test('graduation.md specifies Defer-all shorthand (A key)', () => {
content = content || readWorkflow('graduation.md');
assert.ok(
content.includes('Defer all') || content.includes('[Defer all]'),
'graduation.md must document the Defer all shorthand for first-run batches'
);
});
});
describe('enh-2430 — extract_learnings.md graduated: field', () => {
test('extract_learnings.md documents optional graduated: annotation', () => {
const content = readWorkflow('extract_learnings.md');
assert.ok(
content.includes('graduated:') || content.includes('Graduated:'),
'extract_learnings.md must document optional graduated: field'
);
});
test('extract_learnings.md clarifies graduated: is written only by graduation workflow', () => {
const content = readWorkflow('extract_learnings.md');
assert.ok(
content.includes('graduation workflow') || content.includes('graduation.md'),
'extract_learnings.md must clarify that graduated: is written only by graduation.md'
);
});
});
describe('enh-2430 — INVENTORY sync', () => {
test('INVENTORY.md lists graduation.md', () => {
const inventory = fs.readFileSync(
path.join(__dirname, '../docs/INVENTORY.md'), 'utf-8'
);
assert.ok(inventory.includes('graduation.md'), 'INVENTORY.md must list graduation.md');
});
test('INVENTORY-MANIFEST.json includes graduation.md', () => {
const manifest = JSON.parse(
fs.readFileSync(path.join(__dirname, '../docs/INVENTORY-MANIFEST.json'), 'utf-8')
);
assert.ok(
manifest.families.workflows.includes('graduation.md'),
'INVENTORY-MANIFEST.json must include graduation.md in workflows array'
);
});
});