@sabaiway/agent-workflow-kit 1.15.0 → 1.15.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/SKILL.md +3 -2
- package/capability.json +1 -1
- package/package.json +5 -2
- package/references/contracts.md +2 -0
- package/bin/install.test.mjs +0 -347
- package/tools/delegation.test.mjs +0 -116
- package/tools/detect-backends.test.mjs +0 -444
- package/tools/engine-source.test.mjs +0 -280
- package/tools/family-registry.test.mjs +0 -279
- package/tools/fs-safe.test.mjs +0 -346
- package/tools/hide-footprint.integration.test.mjs +0 -168
- package/tools/hide-footprint.test.mjs +0 -463
- package/tools/inject-methodology.test.mjs +0 -689
- package/tools/known-footprint.test.mjs +0 -271
- package/tools/manifest/fixtures/bad-available/SKILL.md +0 -7
- package/tools/manifest/fixtures/bad-available/capability.json +0 -10
- package/tools/manifest/fixtures/detect-array/SKILL.md +0 -7
- package/tools/manifest/fixtures/detect-array/capability.json +0 -10
- package/tools/manifest/fixtures/malformed-json/capability.json +0 -1
- package/tools/manifest/fixtures/metadata-version/SKILL.md +0 -10
- package/tools/manifest/fixtures/metadata-version/capability.json +0 -9
- package/tools/manifest/fixtures/missing-key/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-key/capability.json +0 -8
- package/tools/manifest/fixtures/missing-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-source/capability.json +0 -11
- package/tools/manifest/fixtures/nested-version-decoy/SKILL.md +0 -10
- package/tools/manifest/fixtures/nested-version-decoy/capability.json +0 -9
- package/tools/manifest/fixtures/null-root/capability.json +0 -1
- package/tools/manifest/fixtures/provides-roles-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/provides-roles-mismatch/bin/run.sh +0 -2
- package/tools/manifest/fixtures/provides-roles-mismatch/capability.json +0 -11
- package/tools/manifest/fixtures/stub/capability.json +0 -10
- package/tools/manifest/fixtures/traversal-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/traversal-source/capability.json +0 -11
- package/tools/manifest/fixtures/unknown-schema/capability.json +0 -9
- package/tools/manifest/fixtures/valid/SKILL.md +0 -10
- package/tools/manifest/fixtures/valid/bin/run.sh +0 -3
- package/tools/manifest/fixtures/valid/capability.json +0 -18
- package/tools/manifest/fixtures/version-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/version-mismatch/capability.json +0 -9
- package/tools/manifest/fixtures/win-absolute-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/win-absolute-source/capability.json +0 -11
- package/tools/manifest/validate.test.mjs +0 -73
- package/tools/procedures.test.mjs +0 -303
- package/tools/recipes.test.mjs +0 -538
- package/tools/release-scan.test.mjs +0 -41
- package/tools/setup-backends.test.mjs +0 -500
- package/tools/uninstall.integration.test.mjs +0 -144
- package/tools/uninstall.test.mjs +0 -401
- package/tools/velocity-profile.test.mjs +0 -496
|
@@ -1,689 +0,0 @@
|
|
|
1
|
-
import { describe, it, after } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync, writeFileSync, mkdirSync, mkdtempSync, rmSync, chmodSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { execFileSync } from 'node:child_process';
|
|
8
|
-
import {
|
|
9
|
-
injectMethodology,
|
|
10
|
-
findSlot,
|
|
11
|
-
extractSlot,
|
|
12
|
-
ensureSlot,
|
|
13
|
-
reconcileSlot,
|
|
14
|
-
slotNeedsFill,
|
|
15
|
-
METHODOLOGY_ANCHOR,
|
|
16
|
-
EMPTY_SLOT,
|
|
17
|
-
AGENTS_MD_CAP,
|
|
18
|
-
START_MARKER,
|
|
19
|
-
END_MARKER,
|
|
20
|
-
ORCH_START_MARKER,
|
|
21
|
-
ORCH_END_MARKER,
|
|
22
|
-
ORCHESTRATION_DESCRIPTOR,
|
|
23
|
-
findMarkerSlot,
|
|
24
|
-
extractMarkerSlot,
|
|
25
|
-
reconcileMarkerSlot,
|
|
26
|
-
methodologyProceduresHint,
|
|
27
|
-
PROCEDURES_POINTER,
|
|
28
|
-
} from './inject-methodology.mjs';
|
|
29
|
-
|
|
30
|
-
// Read the orchestration slot's content from a reconciled entry point.
|
|
31
|
-
const extractOrch = (text) => extractMarkerSlot(text, ORCHESTRATION_DESCRIPTOR);
|
|
32
|
-
const hasOrchSlot = (text) => findMarkerSlot(text, ORCHESTRATION_DESCRIPTOR).state === 'ok';
|
|
33
|
-
|
|
34
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
35
|
-
const SCRIPT = join(HERE, 'inject-methodology.mjs');
|
|
36
|
-
|
|
37
|
-
// The bounded methodology fragment is read LIVE from the installed engine now (the kit mirror is
|
|
38
|
-
// retired in Plan 3D), so this suite no longer reads a bundled methodology-slot.md. Tests use an
|
|
39
|
-
// inline fragment and, for the live-read CLI cases, an on-the-fly engine fixture that ships exactly
|
|
40
|
-
// this fragment — keeping the kit suite decoupled from the sibling engine's on-disk presence.
|
|
41
|
-
// Single-line (like the canonical fragment) so byte-equality holds in both LF and CRLF documents.
|
|
42
|
-
const FRAGMENT =
|
|
43
|
-
'> **Workflow methodology (test fixture)** — plan → execute → review. Plans are ephemeral, gitignored, never committed; every Plan ends with a mandatory **Phase: Cleanup**.\n';
|
|
44
|
-
// The SECOND bounded fragment (Plan 4) — distinct content so a test can tell which slot got which.
|
|
45
|
-
const ORCH_FRAGMENT =
|
|
46
|
-
'> **Orchestration recipes (test fixture)** — Solo / Reviewed / Council / Delegated; pick one with `/agent-workflow-kit recipes`.\n';
|
|
47
|
-
|
|
48
|
-
// Temp dirs created by the fixtures below — cleaned up once after the whole file.
|
|
49
|
-
const tmpDirs = [];
|
|
50
|
-
after(() => tmpDirs.forEach((d) => rmSync(d, { recursive: true, force: true })));
|
|
51
|
-
|
|
52
|
-
// A minimal but VALID installed-engine fixture: a methodology-engine capability.json + a SKILL.md
|
|
53
|
-
// whose metadata.version matches it (the validator's authoritative version source when there is no
|
|
54
|
-
// package.json) + BOTH live fragments (methodology + orchestration). detectEngine accepts it; pass
|
|
55
|
-
// `orchFragment = null` to model an OLDER engine (<1.2.0) that ships no orchestration fragment.
|
|
56
|
-
const makeEngineFixture = (fragment = FRAGMENT, version = '1.0.0', orchFragment = ORCH_FRAGMENT) => {
|
|
57
|
-
const dir = mkdtempSync(join(tmpdir(), 'engine-fixture-'));
|
|
58
|
-
tmpDirs.push(dir);
|
|
59
|
-
const manifest = {
|
|
60
|
-
family: 'agent-workflow',
|
|
61
|
-
schema: 1,
|
|
62
|
-
name: 'agent-workflow-engine',
|
|
63
|
-
kind: 'methodology-engine',
|
|
64
|
-
version,
|
|
65
|
-
available: true,
|
|
66
|
-
provides: ['plan'],
|
|
67
|
-
roles: {},
|
|
68
|
-
};
|
|
69
|
-
writeFileSync(join(dir, 'capability.json'), JSON.stringify(manifest, null, 2));
|
|
70
|
-
writeFileSync(join(dir, 'SKILL.md'), `---\nname: agent-workflow-engine\nmetadata:\n version: '${version}'\n---\n# engine\n`);
|
|
71
|
-
mkdirSync(join(dir, 'references'), { recursive: true });
|
|
72
|
-
writeFileSync(join(dir, 'references', 'methodology-slot.md'), fragment);
|
|
73
|
-
if (orchFragment != null) writeFileSync(join(dir, 'references', 'orchestration-slot.md'), orchFragment);
|
|
74
|
-
return dir;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const ENGINE = makeEngineFixture();
|
|
78
|
-
// A path that is guaranteed NOT to be a valid engine — proves the no-op / explicit-override paths
|
|
79
|
-
// never consult the engine, and drives the fail-loud STOP.
|
|
80
|
-
const NO_ENGINE = join(tmpdir(), `definitely-no-engine-${process.pid}`);
|
|
81
|
-
const withEngine = (engineDir) => ({ ...process.env, AGENT_WORKFLOW_ENGINE_DIR: engineDir });
|
|
82
|
-
|
|
83
|
-
const wrap = (inner) =>
|
|
84
|
-
`# AGENTS.md\n\nprefix bytes\n\n## Session Protocols\n\nintro line.\n\n${START_MARKER}${inner}${END_MARKER}\n\n## Hard Constraints\n\nsuffix bytes\n`;
|
|
85
|
-
|
|
86
|
-
// An entry point carrying BOTH reconciled slots (the orchestration pair sits right under the
|
|
87
|
-
// methodology pair, as the descriptor anchors it) — the deployed shape after a dual-slot reconcile.
|
|
88
|
-
const wrapDual = (methInner, orchInner) =>
|
|
89
|
-
`# AGENTS.md\n\nprefix bytes\n\n## Session Protocols\n\nintro line.\n\n${START_MARKER}${methInner}${END_MARKER}\n${ORCH_START_MARKER}${orchInner}${ORCH_END_MARKER}\n\n## Hard Constraints\n\nsuffix bytes\n`;
|
|
90
|
-
|
|
91
|
-
// The exact Session-Protocols line both deployed templates carry — the slot anchor.
|
|
92
|
-
const ANCHOR_LINE =
|
|
93
|
-
'Start-of-session, during-work, and task-completion procedures live in [`docs/ai/agent_rules.md`](./docs/ai/agent_rules.md) §1. **Read it before any code change.**';
|
|
94
|
-
|
|
95
|
-
// A pre-slot (markerless) entry point that still carries the Session-Protocols anchor line —
|
|
96
|
-
// the realistic shape of a legacy deployment the upgrade reconciliation must add a slot to.
|
|
97
|
-
const legacyWithAnchor = (nl = '\n') =>
|
|
98
|
-
[
|
|
99
|
-
'# AGENTS.md',
|
|
100
|
-
'',
|
|
101
|
-
'prefix bytes',
|
|
102
|
-
'',
|
|
103
|
-
'## 🚀 Session Protocols',
|
|
104
|
-
'',
|
|
105
|
-
ANCHOR_LINE,
|
|
106
|
-
'',
|
|
107
|
-
'---',
|
|
108
|
-
'',
|
|
109
|
-
'## 🚫 Hard Constraints',
|
|
110
|
-
'',
|
|
111
|
-
'suffix bytes',
|
|
112
|
-
'',
|
|
113
|
-
].join(nl);
|
|
114
|
-
|
|
115
|
-
const countMatches = (text, re) => (text.match(new RegExp(re.source, 'gm')) || []).length;
|
|
116
|
-
|
|
117
|
-
describe('findSlot — marker classification', () => {
|
|
118
|
-
it('one ordered pair → ok', () => {
|
|
119
|
-
assert.equal(findSlot(wrap('\n')).state, 'ok');
|
|
120
|
-
});
|
|
121
|
-
it('no markers → absent', () => {
|
|
122
|
-
assert.equal(findSlot('# AGENTS.md\nno markers here\n').state, 'absent');
|
|
123
|
-
});
|
|
124
|
-
it('duplicate pair → malformed', () => {
|
|
125
|
-
const text = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
|
|
126
|
-
assert.equal(findSlot(text).state, 'malformed');
|
|
127
|
-
});
|
|
128
|
-
it('single start only → malformed', () => {
|
|
129
|
-
assert.equal(findSlot(`x\n${START_MARKER}\ny\n`).state, 'malformed');
|
|
130
|
-
});
|
|
131
|
-
it('single end only → malformed', () => {
|
|
132
|
-
assert.equal(findSlot(`x\n${END_MARKER}\ny\n`).state, 'malformed');
|
|
133
|
-
});
|
|
134
|
-
it('reversed (end before start) → malformed', () => {
|
|
135
|
-
assert.equal(findSlot(`${END_MARKER}\nmiddle\n${START_MARKER}\n`).state, 'malformed');
|
|
136
|
-
});
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
describe('injectMethodology — byte preservation', () => {
|
|
140
|
-
it('injects into an empty slot, preserving prefix/suffix exactly', () => {
|
|
141
|
-
const input = wrap('\n');
|
|
142
|
-
const out = injectMethodology(input, FRAGMENT);
|
|
143
|
-
assert.equal(out.status, 'injected');
|
|
144
|
-
assert.ok(out.text.startsWith('# AGENTS.md\n\nprefix bytes\n\n## Session Protocols\n\nintro line.\n\n'));
|
|
145
|
-
assert.ok(out.text.endsWith('\n\n## Hard Constraints\n\nsuffix bytes\n'));
|
|
146
|
-
assert.ok(out.text.includes(FRAGMENT.trim()));
|
|
147
|
-
// markers themselves are preserved
|
|
148
|
-
assert.equal((out.text.match(new RegExp(START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length, 1);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('is idempotent — re-injecting the same fragment is stable', () => {
|
|
152
|
-
const once = injectMethodology(wrap('\n'), FRAGMENT).text;
|
|
153
|
-
const twice = injectMethodology(once, FRAGMENT).text;
|
|
154
|
-
assert.equal(twice, once);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('overwrites a previously-filled slot (bootstrap composition), preserving outside bytes', () => {
|
|
158
|
-
const filled = wrap('\nstale content\n');
|
|
159
|
-
const out = injectMethodology(filled, FRAGMENT);
|
|
160
|
-
assert.equal(out.status, 'injected');
|
|
161
|
-
assert.ok(!out.text.includes('stale content'));
|
|
162
|
-
assert.ok(out.text.endsWith('\n\n## Hard Constraints\n\nsuffix bytes\n'));
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('rejects a fragment that itself contains a marker (would nest/duplicate the slot)', () => {
|
|
166
|
-
const out = injectMethodology(wrap('\n'), `bad ${START_MARKER} fragment`);
|
|
167
|
-
assert.equal(out.status, 'error');
|
|
168
|
-
assert.equal(out.text, wrap('\n'));
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('refuses to bust the line cap (maxLines) instead of silently overflowing it', () => {
|
|
172
|
-
const huge = Array.from({ length: 40 }, (_, i) => `methodology line ${i}`).join('\n');
|
|
173
|
-
const out = injectMethodology(wrap('\n'), huge, { maxLines: 20 });
|
|
174
|
-
assert.equal(out.status, 'error');
|
|
175
|
-
assert.match(out.error, /cap 20/);
|
|
176
|
-
assert.equal(out.text, wrap('\n')); // unchanged
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
it('absent markers → no-op, returns input byte-for-byte', () => {
|
|
180
|
-
const input = '# AGENTS.md\nlegacy file, no slot\n';
|
|
181
|
-
const out = injectMethodology(input, FRAGMENT);
|
|
182
|
-
assert.equal(out.status, 'noop-absent');
|
|
183
|
-
assert.equal(out.text, input);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
for (const [label, input] of [
|
|
187
|
-
['duplicate pair', `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`],
|
|
188
|
-
['single start', `head\n${START_MARKER}\ntail\n`],
|
|
189
|
-
['single end', `head\n${END_MARKER}\ntail\n`],
|
|
190
|
-
['reversed', `${END_MARKER}\nx\n${START_MARKER}\n`],
|
|
191
|
-
]) {
|
|
192
|
-
it(`malformed (${label}) → error, returns input byte-for-byte`, () => {
|
|
193
|
-
const out = injectMethodology(input, FRAGMENT);
|
|
194
|
-
assert.equal(out.status, 'error');
|
|
195
|
-
assert.equal(out.text, input); // never edits a malformed slot
|
|
196
|
-
});
|
|
197
|
-
}
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe('extractSlot — preserve-on-upgrade inverse', () => {
|
|
201
|
-
it('returns the bytes strictly between the markers', () => {
|
|
202
|
-
assert.equal(extractSlot(wrap('\nkeep me\n')), '\nkeep me\n');
|
|
203
|
-
});
|
|
204
|
-
it('null on absent/malformed', () => {
|
|
205
|
-
assert.equal(extractSlot('no markers'), null);
|
|
206
|
-
assert.equal(extractSlot(`${START_MARKER}\n${START_MARKER}\n${END_MARKER}\n`), null);
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('post-injection cap — AGENTS.md stays under its line budget', () => {
|
|
211
|
-
it('injecting the bounded fragment into the real memory template keeps AGENTS.md ≤ 100 lines', () => {
|
|
212
|
-
const template = readFileSync(
|
|
213
|
-
join(HERE, '..', '..', 'agent-workflow-memory', 'references', 'templates', 'AGENTS.md'),
|
|
214
|
-
'utf8',
|
|
215
|
-
);
|
|
216
|
-
const out = injectMethodology(template, FRAGMENT);
|
|
217
|
-
assert.equal(out.status, 'injected');
|
|
218
|
-
const lines = out.text.split('\n').length - (out.text.endsWith('\n') ? 1 : 0);
|
|
219
|
-
assert.ok(lines <= 100, `AGENTS.md would be ${lines} lines after injection (cap 100)`);
|
|
220
|
-
});
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
describe('METHODOLOGY_ANCHOR — locating the Session-Protocols slot position', () => {
|
|
224
|
-
it('matches exactly one line in a deployed-style markerless entry point', () => {
|
|
225
|
-
assert.equal(countMatches(legacyWithAnchor(), METHODOLOGY_ANCHOR), 1);
|
|
226
|
-
});
|
|
227
|
-
it('matches exactly one line in BOTH shipped templates', () => {
|
|
228
|
-
const memTmpl = readFileSync(
|
|
229
|
-
join(HERE, '..', '..', 'agent-workflow-memory', 'references', 'templates', 'AGENTS.md'),
|
|
230
|
-
'utf8',
|
|
231
|
-
);
|
|
232
|
-
const kitTmpl = readFileSync(join(HERE, '..', 'references', 'templates', 'AGENTS.md'), 'utf8');
|
|
233
|
-
assert.equal(countMatches(memTmpl, METHODOLOGY_ANCHOR), 1, 'memory template has exactly one anchor');
|
|
234
|
-
assert.equal(countMatches(kitTmpl, METHODOLOGY_ANCHOR), 1, 'kit fallback template has exactly one anchor');
|
|
235
|
-
});
|
|
236
|
-
it('does not match an entry point that lacks the Session-Protocols line', () => {
|
|
237
|
-
assert.equal(countMatches('# AGENTS.md\nno session protocols here\n', METHODOLOGY_ANCHOR), 0);
|
|
238
|
-
});
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
describe('EMPTY_SLOT — the canonical empty marker pair', () => {
|
|
242
|
-
it('is exactly the start+end markers joined by a newline', () => {
|
|
243
|
-
assert.equal(EMPTY_SLOT, `${START_MARKER}\n${END_MARKER}`);
|
|
244
|
-
assert.equal(findSlot(EMPTY_SLOT).state, 'ok');
|
|
245
|
-
assert.equal(extractSlot(EMPTY_SLOT).trim(), '');
|
|
246
|
-
});
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
describe('ensureSlot — idempotent slot presence (insert at the anchor only when absent)', () => {
|
|
250
|
-
it('present (one ok pair) → status present, bytes unchanged', () => {
|
|
251
|
-
const input = wrap('\nfilled\n');
|
|
252
|
-
const out = ensureSlot(input);
|
|
253
|
-
assert.equal(out.status, 'present');
|
|
254
|
-
assert.equal(out.text, input);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('malformed slot → status error, never edits', () => {
|
|
258
|
-
const input = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
|
|
259
|
-
const out = ensureSlot(input);
|
|
260
|
-
assert.equal(out.status, 'error');
|
|
261
|
-
assert.equal(out.text, input);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('absent + exactly one anchor → inserts EMPTY_SLOT after the anchor line, preserving bytes', () => {
|
|
265
|
-
const input = legacyWithAnchor();
|
|
266
|
-
const out = ensureSlot(input);
|
|
267
|
-
assert.equal(out.status, 'inserted');
|
|
268
|
-
assert.equal(findSlot(out.text).state, 'ok', 'a well-formed slot now exists');
|
|
269
|
-
assert.equal(extractSlot(out.text).trim(), '', 'the inserted slot is empty');
|
|
270
|
-
// the slot lands right after the anchor line
|
|
271
|
-
assert.match(out.text, new RegExp(`Read it before any code change\\.\\*\\*\\n+${START_MARKER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
|
|
272
|
-
// every original line survives
|
|
273
|
-
for (const line of ['# AGENTS.md', 'prefix bytes', ANCHOR_LINE, '## 🚫 Hard Constraints', 'suffix bytes']) {
|
|
274
|
-
assert.ok(out.text.includes(line), `original line preserved: ${line}`);
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('absent + zero anchor → status error with an actionable message, never edits', () => {
|
|
279
|
-
const input = '# AGENTS.md\n\nno anchor at all\n';
|
|
280
|
-
const out = ensureSlot(input);
|
|
281
|
-
assert.equal(out.status, 'error');
|
|
282
|
-
assert.equal(out.text, input);
|
|
283
|
-
assert.match(out.error, /anchor/i);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('absent + multiple anchors → status error (refuses to guess), never edits', () => {
|
|
287
|
-
const input = `${ANCHOR_LINE}\n\nsome text\n\n${ANCHOR_LINE}\n`;
|
|
288
|
-
const out = ensureSlot(input);
|
|
289
|
-
assert.equal(out.status, 'error');
|
|
290
|
-
assert.equal(out.text, input);
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
it('preserves CRLF newline style when inserting', () => {
|
|
294
|
-
const input = legacyWithAnchor('\r\n');
|
|
295
|
-
const out = ensureSlot(input);
|
|
296
|
-
assert.equal(out.status, 'inserted');
|
|
297
|
-
assert.equal(findSlot(out.text).state, 'ok');
|
|
298
|
-
assert.ok(out.text.includes(`${START_MARKER}\r\n${END_MARKER}`), 'markers use CRLF');
|
|
299
|
-
assert.ok(!/[^\r]\n/.test(out.text), 'no lone LF introduced into a CRLF document');
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('is idempotent — a second ensureSlot finds the slot present and changes nothing', () => {
|
|
303
|
-
const once = ensureSlot(legacyWithAnchor()).text;
|
|
304
|
-
const twice = ensureSlot(once);
|
|
305
|
-
assert.equal(twice.status, 'present');
|
|
306
|
-
assert.equal(twice.text, once);
|
|
307
|
-
});
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
describe('reconcileSlot — ensure + inject-if-empty + cap, as one atomic policy', () => {
|
|
311
|
-
it('markerless legacy (with anchor) → reconciled-inserted, slot filled, outside bytes preserved', () => {
|
|
312
|
-
const input = legacyWithAnchor();
|
|
313
|
-
const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
314
|
-
assert.equal(out.status, 'reconciled-inserted');
|
|
315
|
-
assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim(), 'slot now carries the fragment');
|
|
316
|
-
assert.ok(out.text.includes(ANCHOR_LINE), 'anchor preserved');
|
|
317
|
-
assert.ok(out.text.includes('## 🚫 Hard Constraints'), 'suffix preserved');
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('present empty slot → reconciled-filled', () => {
|
|
321
|
-
const out = reconcileSlot(wrap('\n'), FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
322
|
-
assert.equal(out.status, 'reconciled-filled');
|
|
323
|
-
assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim());
|
|
324
|
-
});
|
|
325
|
-
|
|
326
|
-
it('present customized/filled slot → present-filled, preserved verbatim (byte-for-byte)', () => {
|
|
327
|
-
const custom = wrap('\nuser-authored methodology notes\nsecond line\n');
|
|
328
|
-
const out = reconcileSlot(custom, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
329
|
-
assert.equal(out.status, 'present-filled');
|
|
330
|
-
assert.equal(out.text, custom, 'a filled slot is never overwritten');
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('malformed slot → error, input returned byte-for-byte', () => {
|
|
334
|
-
const input = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
|
|
335
|
-
const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
336
|
-
assert.equal(out.status, 'error');
|
|
337
|
-
assert.equal(out.text, input);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('markerless with no anchor → error, input unchanged (never guesses placement)', () => {
|
|
341
|
-
const input = '# AGENTS.md\n\nno slot, no anchor\n';
|
|
342
|
-
const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
343
|
-
assert.equal(out.status, 'error');
|
|
344
|
-
assert.equal(out.text, input);
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
it('over-cap result → error, input unchanged (atomic: discards the intermediate slot insert)', () => {
|
|
348
|
-
const input = legacyWithAnchor();
|
|
349
|
-
const out = reconcileSlot(input, FRAGMENT, { maxLines: 5 });
|
|
350
|
-
assert.equal(out.status, 'error');
|
|
351
|
-
assert.equal(out.text, input);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('produces a clean CRLF document when inserting + filling (no lone LF)', () => {
|
|
355
|
-
// ensureSlot preserves the document newline style for the markers, and injectMethodology frames
|
|
356
|
-
// the LF-canonical fragment with the document's EOL — so a CRLF document stays uniformly CRLF.
|
|
357
|
-
const input = legacyWithAnchor('\r\n');
|
|
358
|
-
const out = reconcileSlot(input, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
359
|
-
assert.equal(out.status, 'reconciled-inserted');
|
|
360
|
-
assert.equal(extractSlot(out.text).trim(), FRAGMENT.trim());
|
|
361
|
-
assert.ok(out.text.includes(`${ANCHOR_LINE}\r\n`), 'anchor line keeps CRLF');
|
|
362
|
-
assert.ok(!/[^\r]\n/.test(out.text), 'no lone LF introduced into a CRLF document');
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
it('present filled slot over the line cap → error, input returned unchanged', () => {
|
|
366
|
-
const bigSlot = `\n${Array.from({ length: 30 }, (_, i) => `note ${i}`).join('\n')}\n`;
|
|
367
|
-
const filled = wrap(bigSlot);
|
|
368
|
-
const out = reconcileSlot(filled, FRAGMENT, { maxLines: 10 });
|
|
369
|
-
assert.equal(out.status, 'error');
|
|
370
|
-
assert.equal(out.text, filled, 'an over-cap entry point is surfaced, not silently accepted');
|
|
371
|
-
assert.match(out.error, /cap 10/);
|
|
372
|
-
});
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
describe('slotNeedsFill — lazy-read predicate (matches reconcileSlot fill decision)', () => {
|
|
376
|
-
it('present empty slot → true', () => {
|
|
377
|
-
assert.equal(slotNeedsFill(wrap('\n')), true);
|
|
378
|
-
});
|
|
379
|
-
it('markerless legacy with one anchor (insertable empty slot) → true', () => {
|
|
380
|
-
assert.equal(slotNeedsFill(legacyWithAnchor()), true);
|
|
381
|
-
});
|
|
382
|
-
it('present filled/customized slot → false (fragment not needed)', () => {
|
|
383
|
-
assert.equal(slotNeedsFill(wrap('\nuser notes\n')), false);
|
|
384
|
-
});
|
|
385
|
-
it('malformed slot → false (reconcileSlot error path fires, not a fill)', () => {
|
|
386
|
-
assert.equal(slotNeedsFill(`${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`), false);
|
|
387
|
-
});
|
|
388
|
-
it('markerless with no anchor → false (cannot insert; reconcile errors without the engine)', () => {
|
|
389
|
-
assert.equal(slotNeedsFill('# AGENTS.md\n\nno anchor here\n'), false);
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
// slotNeedsFill and reconcileSlot share the SAME emptiness primitives (ensureSlot + extractSlot), so
|
|
393
|
-
// the lazy "read the engine only when needed" guard cannot disagree with reconcileSlot's actual fill
|
|
394
|
-
// decision. Pin that equivalence across representative inputs: needsFill === true IFF reconcile fills
|
|
395
|
-
// (reconciled-filled / reconciled-inserted); needsFill === false IFF reconcile does NOT fill
|
|
396
|
-
// (present-filled / error). This forecloses any future divergence that could silently drop a slot.
|
|
397
|
-
it('agrees with reconcileSlot across every slot state (no divergence → no silent drop)', () => {
|
|
398
|
-
const cases = [
|
|
399
|
-
wrap('\n'), // present empty slot → fill
|
|
400
|
-
legacyWithAnchor(), // markerless + anchor → insert + fill
|
|
401
|
-
wrap('\nuser notes\n'), // filled slot → no fill
|
|
402
|
-
`${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`, // malformed → no fill (error)
|
|
403
|
-
'# AGENTS.md\n\nno anchor here\n', // no anchor → no fill (error)
|
|
404
|
-
];
|
|
405
|
-
for (const text of cases) {
|
|
406
|
-
const needs = slotNeedsFill(text);
|
|
407
|
-
const result = reconcileSlot(text, FRAGMENT, { maxLines: AGENTS_MD_CAP });
|
|
408
|
-
const filled = result.status === 'reconciled-filled' || result.status === 'reconciled-inserted';
|
|
409
|
-
assert.equal(needs, filled, `slotNeedsFill (${needs}) must match whether reconcile fills (${result.status})`);
|
|
410
|
-
}
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
describe('reconcile CLI — atomic ensure+inject-if-empty+cap, reading the fragment LIVE from the engine', () => {
|
|
415
|
-
const withTempAgents = (contents, run) => {
|
|
416
|
-
const dir = mkdtempSync(join(tmpdir(), 'reconcile-cli-'));
|
|
417
|
-
const agents = join(dir, 'AGENTS.md');
|
|
418
|
-
writeFileSync(agents, contents);
|
|
419
|
-
try {
|
|
420
|
-
return run(agents);
|
|
421
|
-
} finally {
|
|
422
|
-
rmSync(dir, { recursive: true, force: true });
|
|
423
|
-
}
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
it('markerless legacy (with anchor) → BOTH slots inserted + filled from their own live engine fragments (exit 0)', () => {
|
|
427
|
-
withTempAgents(legacyWithAnchor(), (agents) => {
|
|
428
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(ENGINE) });
|
|
429
|
-
const out = readFileSync(agents, 'utf8');
|
|
430
|
-
assert.equal(findSlot(out).state, 'ok');
|
|
431
|
-
assert.equal(extractSlot(out).trim(), FRAGMENT.trim(), 'methodology slot filled from the methodology fragment');
|
|
432
|
-
assert.ok(hasOrchSlot(out), 'the orchestration slot was inserted below the methodology pair');
|
|
433
|
-
assert.equal(extractOrch(out).trim(), ORCH_FRAGMENT.trim(), 'orchestration slot filled from the orchestration fragment');
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
it('present empty methodology slot → BOTH slots filled; each from its OWN engine fragment (exit 0)', () => {
|
|
438
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
439
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(ENGINE) });
|
|
440
|
-
const out = readFileSync(agents, 'utf8');
|
|
441
|
-
assert.equal(extractSlot(out).trim(), FRAGMENT.trim());
|
|
442
|
-
assert.equal(extractOrch(out).trim(), ORCH_FRAGMENT.trim());
|
|
443
|
-
// Per-slot fragment sourcing: the two slots carry DIFFERENT content (not both the methodology one).
|
|
444
|
-
assert.notEqual(extractSlot(out).trim(), extractOrch(out).trim());
|
|
445
|
-
});
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
it('BOTH slots already filled → zero-diff no-op WITHOUT consulting the engine (engine absent, exit 0)', () => {
|
|
449
|
-
const custom = wrapDual('\nuser meth notes\n', '\nuser orch notes\n');
|
|
450
|
-
withTempAgents(custom, (agents) => {
|
|
451
|
-
// Engine pointed at a path that does not exist — a fully-filled entry point must NOT require it.
|
|
452
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) });
|
|
453
|
-
assert.equal(readFileSync(agents, 'utf8'), custom);
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
it('present empty slot + engine ABSENT → hard STOP (nonzero) printing the install command, file unchanged', () => {
|
|
458
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
459
|
-
const err = (() => {
|
|
460
|
-
try {
|
|
461
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) });
|
|
462
|
-
return null;
|
|
463
|
-
} catch (e) {
|
|
464
|
-
return e;
|
|
465
|
-
}
|
|
466
|
-
})();
|
|
467
|
-
assert.ok(err, 'expected a non-zero exit when a fill is needed but the engine is absent');
|
|
468
|
-
const stderr = String(err.stderr);
|
|
469
|
-
assert.match(stderr, /methodology engine not found\/invalid/);
|
|
470
|
-
assert.match(stderr, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
471
|
-
assert.equal(readFileSync(agents, 'utf8'), wrap('\n'), 'no partial write on STOP');
|
|
472
|
-
});
|
|
473
|
-
});
|
|
474
|
-
|
|
475
|
-
it('explicit [fragment.md] override fills methodology ONLY and skips both engine + the orchestration slot', () => {
|
|
476
|
-
const override = '> custom override fragment line\n';
|
|
477
|
-
const fdir = mkdtempSync(join(tmpdir(), 'frag-'));
|
|
478
|
-
tmpDirs.push(fdir);
|
|
479
|
-
const fpath = join(fdir, 'frag.md');
|
|
480
|
-
writeFileSync(fpath, override);
|
|
481
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
482
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents, fpath], { stdio: 'pipe', env: withEngine(NO_ENGINE) });
|
|
483
|
-
const out = readFileSync(agents, 'utf8');
|
|
484
|
-
assert.equal(extractSlot(out).trim(), override.trim(), 'methodology filled from the explicit fragment');
|
|
485
|
-
assert.ok(!hasOrchSlot(out), 'an explicit single-file override binds methodology only — no orchestration slot added');
|
|
486
|
-
});
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
it('dual-block cap → orchestration pointer is SOFT-skipped (reported), the file is byte-unchanged (exit 0)', () => {
|
|
490
|
-
// A 100-line entry point with a FILLED methodology slot + an EMPTY orchestration slot: filling the
|
|
491
|
-
// orchestration slot would push it to 101 > 100, so it is skipped loudly while methodology stays.
|
|
492
|
-
const head = [
|
|
493
|
-
'# AGENTS.md',
|
|
494
|
-
'',
|
|
495
|
-
'## Session Protocols',
|
|
496
|
-
'',
|
|
497
|
-
'Read it before any code change.',
|
|
498
|
-
'',
|
|
499
|
-
START_MARKER,
|
|
500
|
-
FRAGMENT.trim(),
|
|
501
|
-
END_MARKER,
|
|
502
|
-
ORCH_START_MARKER,
|
|
503
|
-
ORCH_END_MARKER,
|
|
504
|
-
];
|
|
505
|
-
const pad = Array.from({ length: AGENTS_MD_CAP - head.length }, (_, i) => `pad line ${i}`);
|
|
506
|
-
const atCap = [...head, ...pad].join('\n') + '\n';
|
|
507
|
-
withTempAgents(atCap, (agents) => {
|
|
508
|
-
const out = execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { encoding: 'utf8', env: withEngine(ENGINE) });
|
|
509
|
-
assert.match(out, /skipped/i, 'the orchestration cap-skip is reported, not silent');
|
|
510
|
-
assert.equal(readFileSync(agents, 'utf8'), atCap, 'file byte-unchanged — the orchestration pointer was withheld, methodology already present');
|
|
511
|
-
});
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
it('malformed orchestration pair → hard STOP (nonzero), file byte-unchanged (methodology fine)', () => {
|
|
515
|
-
const malformedOrch =
|
|
516
|
-
`# AGENTS.md\n\nintro line.\n\n${START_MARKER}\n${FRAGMENT.trim()}\n${END_MARKER}\n` +
|
|
517
|
-
`${ORCH_START_MARKER}\n${ORCH_END_MARKER}\n${ORCH_START_MARKER}\n${ORCH_END_MARKER}\n`;
|
|
518
|
-
withTempAgents(malformedOrch, (agents) => {
|
|
519
|
-
assert.throws(() =>
|
|
520
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(ENGINE) }),
|
|
521
|
-
);
|
|
522
|
-
assert.equal(readFileSync(agents, 'utf8'), malformedOrch, 'no partial write on an orchestration STOP');
|
|
523
|
-
});
|
|
524
|
-
});
|
|
525
|
-
|
|
526
|
-
it('engine too old (no orchestration fragment) → methodology filled, orchestration SOFT-skipped (exit 0, not a regression)', () => {
|
|
527
|
-
// A VALID engine that ships methodology-slot.md but NOT orchestration-slot.md (i.e. <1.2.0). The
|
|
528
|
-
// methodology fill must NOT be discarded — only the recipes pointer is withheld, reported, exit 0.
|
|
529
|
-
const oldEngine = makeEngineFixture(FRAGMENT, '1.0.0', null);
|
|
530
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
531
|
-
const out = execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { encoding: 'utf8', env: withEngine(oldEngine) });
|
|
532
|
-
const text = readFileSync(agents, 'utf8');
|
|
533
|
-
assert.equal(extractSlot(text).trim(), FRAGMENT.trim(), 'methodology pointer filled (not discarded by the too-old engine)');
|
|
534
|
-
assert.match(out, /skipped/i, 'the orchestration skip is reported (not silent)');
|
|
535
|
-
assert.match(out, /too old/i, 'the skip names the too-old-engine reason');
|
|
536
|
-
assert.ok(!hasOrchSlot(text), 'a too-old engine adds no orchestration pointer');
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
it('orchestration fragment PRESENT but unreadable → hard STOP (a corrupt engine is NOT mislabeled "too old")', () => {
|
|
541
|
-
if (process.getuid && process.getuid() === 0) return; // root bypasses 0o000 perms — can't restrict
|
|
542
|
-
const corruptEngine = makeEngineFixture(FRAGMENT, '1.2.0', '> orchestration line\n'); // both fragments present
|
|
543
|
-
const orchPath = join(corruptEngine, 'references', 'orchestration-slot.md');
|
|
544
|
-
chmodSync(orchPath, 0o000);
|
|
545
|
-
let restricted = false;
|
|
546
|
-
try {
|
|
547
|
-
readFileSync(orchPath, 'utf8');
|
|
548
|
-
} catch {
|
|
549
|
-
restricted = true;
|
|
550
|
-
}
|
|
551
|
-
if (!restricted) {
|
|
552
|
-
chmodSync(orchPath, 0o644); // exotic FS / perms ignored → can't exercise this path here
|
|
553
|
-
return;
|
|
554
|
-
}
|
|
555
|
-
try {
|
|
556
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
557
|
-
assert.throws(() =>
|
|
558
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(corruptEngine) }),
|
|
559
|
-
);
|
|
560
|
-
assert.equal(extractSlot(readFileSync(agents, 'utf8')).trim(), '', 'no partial write on a corrupt-fragment STOP');
|
|
561
|
-
});
|
|
562
|
-
} finally {
|
|
563
|
-
chmodSync(orchPath, 0o644); // restore so the after() cleanup can remove the fixture
|
|
564
|
-
}
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('fully absent engine + a fill needed → hard STOP (methodology fragment also unavailable)', () => {
|
|
568
|
-
// Distinct from the too-old case: a fully absent engine cannot supply EITHER fragment, so the
|
|
569
|
-
// methodology slot fill itself STOPs first — the dual-slot reconcile never silently no-ops.
|
|
570
|
-
withTempAgents(wrap('\n'), (agents) => {
|
|
571
|
-
assert.throws(() =>
|
|
572
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) }),
|
|
573
|
-
);
|
|
574
|
-
assert.equal(readFileSync(agents, 'utf8'), wrap('\n'), 'no partial write when the engine is fully absent');
|
|
575
|
-
});
|
|
576
|
-
});
|
|
577
|
-
|
|
578
|
-
it('malformed slot → STOP with non-zero exit, file byte-unchanged (engine never consulted)', () => {
|
|
579
|
-
const malformed = `${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`;
|
|
580
|
-
withTempAgents(malformed, (agents) => {
|
|
581
|
-
assert.throws(() =>
|
|
582
|
-
execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) }),
|
|
583
|
-
);
|
|
584
|
-
assert.equal(readFileSync(agents, 'utf8'), malformed);
|
|
585
|
-
});
|
|
586
|
-
});
|
|
587
|
-
|
|
588
|
-
it('legacy inject mode: markerless AGENTS.md → no-op WITHOUT the engine (exit 0)', () => {
|
|
589
|
-
const markerless = '# AGENTS.md\n\nlegacy, no slot\n';
|
|
590
|
-
withTempAgents(markerless, (agents) => {
|
|
591
|
-
execFileSync(process.execPath, [SCRIPT, agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) });
|
|
592
|
-
assert.equal(readFileSync(agents, 'utf8'), markerless);
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
// Legacy `inject` mode FORCE-OVERWRITES any present (ok) slot — filled or empty — unlike `reconcile`,
|
|
597
|
-
// which preserves a filled slot. So for an ok slot it genuinely NEEDS the fragment and reads the
|
|
598
|
-
// engine; the read-on-`state==='ok'` guard is correct (reading only an EMPTY ok slot would inject ''
|
|
599
|
-
// and WIPE a filled slot). These two tests pin that contract.
|
|
600
|
-
it('legacy inject mode: a FILLED slot is OVERWRITTEN from the live engine (engine present, exit 0)', () => {
|
|
601
|
-
const filled = wrap('\nstale user content\n');
|
|
602
|
-
withTempAgents(filled, (agents) => {
|
|
603
|
-
execFileSync(process.execPath, [SCRIPT, agents], { stdio: 'pipe', env: withEngine(ENGINE) });
|
|
604
|
-
const out = readFileSync(agents, 'utf8');
|
|
605
|
-
assert.equal(extractSlot(out).trim(), FRAGMENT.trim(), 'slot overwritten with the live fragment');
|
|
606
|
-
assert.ok(!out.includes('stale user content'), 'prior slot content replaced');
|
|
607
|
-
});
|
|
608
|
-
});
|
|
609
|
-
|
|
610
|
-
it('legacy inject mode: a present (ok) slot + engine ABSENT → hard STOP, file unchanged', () => {
|
|
611
|
-
const filled = wrap('\nstale user content\n');
|
|
612
|
-
withTempAgents(filled, (agents) => {
|
|
613
|
-
assert.throws(() =>
|
|
614
|
-
execFileSync(process.execPath, [SCRIPT, agents], { stdio: 'pipe', env: withEngine(NO_ENGINE) }),
|
|
615
|
-
);
|
|
616
|
-
assert.equal(readFileSync(agents, 'utf8'), filled, 'no partial write on STOP');
|
|
617
|
-
});
|
|
618
|
-
});
|
|
619
|
-
});
|
|
620
|
-
|
|
621
|
-
// AD-019 §3.1a — the read-only upgrade advisory: a FILLED methodology pointer lacking the procedures
|
|
622
|
-
// route gets a hint (it can't be auto-re-rendered, reconcile preserves a filled slot verbatim); a slot
|
|
623
|
-
// that already routes to procedures, or an empty / absent / malformed slot, is silent. NO mutation.
|
|
624
|
-
describe('methodologyProceduresHint — read-only upgrade advisory (§3.1a)', () => {
|
|
625
|
-
it('a filled methodology slot WITHOUT the procedures route → a hint naming the procedures command', () => {
|
|
626
|
-
const entry = wrap('\n> methodology notes the user wrote, no procedures route here\n');
|
|
627
|
-
const hint = methodologyProceduresHint(entry);
|
|
628
|
-
assert.ok(hint, 'a filled-without-clause slot yields a hint');
|
|
629
|
-
assert.match(hint, new RegExp(PROCEDURES_POINTER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
it('a filled methodology slot that ALREADY routes to procedures → silent (null)', () => {
|
|
633
|
-
const entry = wrap(`\n> methodology — see ${PROCEDURES_POINTER} <activity> for the steps\n`);
|
|
634
|
-
assert.equal(methodologyProceduresHint(entry), null);
|
|
635
|
-
});
|
|
636
|
-
|
|
637
|
-
it('an empty methodology slot → silent (null) — only a filled slot is advised', () => {
|
|
638
|
-
assert.equal(methodologyProceduresHint(wrap('\n')), null);
|
|
639
|
-
});
|
|
640
|
-
|
|
641
|
-
it('an absent / malformed methodology slot → silent (null)', () => {
|
|
642
|
-
assert.equal(methodologyProceduresHint('# AGENTS.md\n\nno slot here\n'), null);
|
|
643
|
-
assert.equal(methodologyProceduresHint(`${START_MARKER}\n${END_MARKER}\n${START_MARKER}\n${END_MARKER}\n`), null);
|
|
644
|
-
});
|
|
645
|
-
|
|
646
|
-
it('is read-only: a pre-filled-without-clause deployment is reported, not mutated, on reconcile (engine absent)', () => {
|
|
647
|
-
// A dual-filled entry point whose methodology lacks the procedures route: reconcile is a zero-diff
|
|
648
|
-
// no-op (filled slots preserved) yet still surfaces the hint on stdout — never rewrites the file.
|
|
649
|
-
const custom = wrapDual('\nuser meth notes, no procedures route\n', '\nuser orch notes\n');
|
|
650
|
-
const dir = mkdtempSync(join(tmpdir(), 'reconcile-hint-'));
|
|
651
|
-
tmpDirs.push(dir);
|
|
652
|
-
const agents = join(dir, 'AGENTS.md');
|
|
653
|
-
writeFileSync(agents, custom);
|
|
654
|
-
const out = execFileSync(process.execPath, [SCRIPT, 'reconcile', agents], { encoding: 'utf8', env: withEngine(NO_ENGINE) });
|
|
655
|
-
assert.equal(readFileSync(agents, 'utf8'), custom, 'the file is byte-unchanged (read-only advisory)');
|
|
656
|
-
assert.match(out, /note:.*procedures/i, 'the upgrade flow surfaces the procedures hint');
|
|
657
|
-
});
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
// §3.2 (kit) — the REAL extended engine fragments must keep the dual-fill ≤ 100 on BOTH deployed
|
|
661
|
-
// templates. The methodology fragment grew a procedures clause (still ONE line, so no extra line), but
|
|
662
|
-
// pin it against the REAL fragments + REAL templates so a future fragment edit that DID add a line is
|
|
663
|
-
// caught here, not in the field.
|
|
664
|
-
describe('real-fragment dual-fill ≤ cap on both deployed templates (§3.2)', () => {
|
|
665
|
-
const ENGINE_REFS = join(HERE, '..', '..', 'agent-workflow-engine', 'references');
|
|
666
|
-
const realMeth = readFileSync(join(ENGINE_REFS, 'methodology-slot.md'), 'utf8');
|
|
667
|
-
const realOrch = readFileSync(join(ENGINE_REFS, 'orchestration-slot.md'), 'utf8');
|
|
668
|
-
const lineCount = (t) => t.split('\n').length - (t.endsWith('\n') ? 1 : 0);
|
|
669
|
-
const TEMPLATES = {
|
|
670
|
-
kit: join(HERE, '..', 'references', 'templates', 'AGENTS.md'),
|
|
671
|
-
memory: join(HERE, '..', '..', 'agent-workflow-memory', 'references', 'templates', 'AGENTS.md'),
|
|
672
|
-
};
|
|
673
|
-
|
|
674
|
-
it('the real methodology fragment carries the procedures route (auto-discovery clause)', () => {
|
|
675
|
-
assert.match(realMeth, new RegExp(PROCEDURES_POINTER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
676
|
-
assert.equal(lineCount(realMeth), 1, 'the methodology fragment stays exactly one content line');
|
|
677
|
-
});
|
|
678
|
-
|
|
679
|
-
for (const [name, path] of Object.entries(TEMPLATES)) {
|
|
680
|
-
it(`${name} template: filling BOTH real fragments stays ≤ ${AGENTS_MD_CAP} lines`, () => {
|
|
681
|
-
const template = readFileSync(path, 'utf8');
|
|
682
|
-
const meth = reconcileSlot(template, realMeth, { maxLines: AGENTS_MD_CAP });
|
|
683
|
-
assert.equal(meth.status, 'reconciled-filled', `${name}: methodology slot fills`);
|
|
684
|
-
const both = reconcileMarkerSlot(meth.text, ORCHESTRATION_DESCRIPTOR, realOrch, { maxLines: AGENTS_MD_CAP });
|
|
685
|
-
assert.equal(both.status, 'reconciled-filled', `${name}: orchestration slot fills`);
|
|
686
|
-
assert.ok(lineCount(both.text) <= AGENTS_MD_CAP, `${name}: dual-filled entry point is ${lineCount(both.text)} lines (cap ${AGENTS_MD_CAP})`);
|
|
687
|
-
});
|
|
688
|
-
}
|
|
689
|
-
});
|