agentic-workflow-manager 9.2.1 → 9.4.0
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/dist/src/commands/plan/index.js +98 -0
- package/dist/src/commands/preflight/checks.js +111 -6
- package/dist/src/commands/preflight/index.js +9 -6
- package/dist/src/core/context-kernel/inspect.js +221 -0
- package/dist/src/core/context-kernel/types.js +2 -0
- package/dist/src/core/currentness/check.js +224 -0
- package/dist/src/core/currentness/types.js +2 -0
- package/dist/src/core/plan/json.js +111 -0
- package/dist/src/core/plan/types.js +2 -0
- package/dist/src/core/plan/validate.js +393 -0
- package/dist/src/core/registries.js +30 -1
- package/dist/src/index.js +7 -1
- package/dist/tests/commands/plan/index.test.js +154 -0
- package/dist/tests/commands/preflight/preflight.test.js +238 -2
- package/dist/tests/core/context-kernel/inspect.test.js +144 -0
- package/dist/tests/core/currentness/check.test.js +293 -0
- package/dist/tests/core/plan/validate.test.js +437 -0
- package/dist/tests/core/registry-manifest.test.js +28 -0
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +38 -0
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +37 -0
- package/package.json +2 -2
|
@@ -10,6 +10,7 @@ const child_process_1 = require("child_process");
|
|
|
10
10
|
const checks_1 = require("../../../src/commands/preflight/checks");
|
|
11
11
|
const preflight_1 = require("../../../src/commands/preflight");
|
|
12
12
|
const run_1 = require("../../../src/commands/sensors/run");
|
|
13
|
+
const check_1 = require("../../../src/core/currentness/check");
|
|
13
14
|
// Only `execSync` (used by `resolveOnPath` to check for `gh`/`glab`) is mocked — `git
|
|
14
15
|
// remote get-url origin` runs for real via `execFileSync` against real tmpdir git repos,
|
|
15
16
|
// same as every other check in this file exercises the real filesystem.
|
|
@@ -21,8 +22,12 @@ jest.mock('../../../src/commands/sensors/run', () => ({
|
|
|
21
22
|
...jest.requireActual('../../../src/commands/sensors/run'),
|
|
22
23
|
runSensors: jest.fn(),
|
|
23
24
|
}));
|
|
25
|
+
jest.mock('../../../src/core/currentness/check', () => ({
|
|
26
|
+
checkCurrentness: jest.fn(),
|
|
27
|
+
}));
|
|
24
28
|
const mockExecSync = child_process_1.execSync;
|
|
25
29
|
const mockRunSensors = run_1.runSensors;
|
|
30
|
+
const mockCheckCurrentness = check_1.checkCurrentness;
|
|
26
31
|
/** Turn a tmpdir into a real git repo with (optionally) an `origin` remote. */
|
|
27
32
|
function gitRepo(dir, remoteUrl) {
|
|
28
33
|
(0, child_process_1.execFileSync)('git', ['init'], { cwd: dir, stdio: 'pipe' });
|
|
@@ -51,19 +56,250 @@ function project(opts = {}) {
|
|
|
51
56
|
const dirs = [];
|
|
52
57
|
const make = (o) => { const d = project(o); dirs.push(d); return d; };
|
|
53
58
|
afterAll(() => dirs.forEach(d => fs_1.default.rmSync(d, { recursive: true, force: true })));
|
|
59
|
+
let previousAwmHome;
|
|
60
|
+
const awmHomes = [];
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
previousAwmHome = process.env.AWM_HOME;
|
|
63
|
+
const awmHome = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-home-'));
|
|
64
|
+
awmHomes.push(awmHome);
|
|
65
|
+
process.env.AWM_HOME = awmHome;
|
|
66
|
+
});
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
if (previousAwmHome === undefined)
|
|
69
|
+
delete process.env.AWM_HOME;
|
|
70
|
+
else
|
|
71
|
+
process.env.AWM_HOME = previousAwmHome;
|
|
72
|
+
});
|
|
73
|
+
afterAll(() => awmHomes.forEach(dir => fs_1.default.rmSync(dir, { recursive: true, force: true })));
|
|
74
|
+
function installRegistry(manifest, name = 'test-registry') {
|
|
75
|
+
const awmHome = process.env.AWM_HOME;
|
|
76
|
+
const root = path_1.default.join(awmHome, 'registries', name);
|
|
77
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills'), { recursive: true });
|
|
78
|
+
const config = fs_1.default.existsSync(path_1.default.join(awmHome, 'registries.json'))
|
|
79
|
+
? JSON.parse(fs_1.default.readFileSync(path_1.default.join(awmHome, 'registries.json'), 'utf8'))
|
|
80
|
+
: [];
|
|
81
|
+
config.push({ name, remote: `https://example.invalid/${name}.git` });
|
|
82
|
+
fs_1.default.writeFileSync(path_1.default.join(awmHome, 'registries.json'), JSON.stringify(config));
|
|
83
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'awm-registry.json'), JSON.stringify(manifest));
|
|
84
|
+
}
|
|
85
|
+
function writeValidKernel(dir) {
|
|
86
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
87
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'CONSTITUTION.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\n<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
88
|
+
const card = path_1.default.join(dir, 'docs', 'awm', 'context', 'releases.md');
|
|
89
|
+
fs_1.default.mkdirSync(path_1.default.dirname(card), { recursive: true });
|
|
90
|
+
fs_1.default.writeFileSync(card, '<!-- awm-context:CTX-RELEASE-001 -->\nrelease rules\n');
|
|
91
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm', 'context'), { recursive: true });
|
|
92
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'context', 'index.json'), JSON.stringify({
|
|
93
|
+
schema: 1,
|
|
94
|
+
kernelFiles: ['AGENTS.md', 'CONSTITUTION.md'],
|
|
95
|
+
maxFixedBytes: 33_740,
|
|
96
|
+
entries: [
|
|
97
|
+
{ id: 'CTX-PROCESS-001', tier: 'kernel', path: 'CONSTITUTION.md', anchor: 'awm-context:CTX-PROCESS-001', when: 'always' },
|
|
98
|
+
{ id: 'CTX-RELEASE-001', tier: 'selective', path: 'docs/awm/context/releases.md', anchor: 'awm-context:CTX-RELEASE-001', when: 'release automation' },
|
|
99
|
+
],
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
function writePartialKernel(dir) {
|
|
103
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n');
|
|
104
|
+
}
|
|
54
105
|
const check = (r, id) => r.checks.find(c => c.id === id);
|
|
55
106
|
describe('preflight', () => {
|
|
56
|
-
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
mockCheckCurrentness.mockResolvedValue({
|
|
109
|
+
checkedAt: '2026-08-26T00:00:00.000Z',
|
|
110
|
+
compatibility: { status: 'not-checked' },
|
|
111
|
+
components: [{
|
|
112
|
+
component: 'cli', installed: '9.3.0', latest: '9.3.0', channel: 'stable',
|
|
113
|
+
source: 'https://registry.npmjs.org/agentic-workflow-manager',
|
|
114
|
+
checkedAt: '2026-08-26T00:00:00.000Z', status: 'current',
|
|
115
|
+
detail: 'Installed version matches npm dist-tags.latest.', remedy: 'No action required.',
|
|
116
|
+
}],
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
afterEach(() => {
|
|
120
|
+
mockRunSensors.mockReset();
|
|
121
|
+
mockCheckCurrentness.mockReset();
|
|
122
|
+
});
|
|
123
|
+
it('does not add a row when no registry declares the schema', async () => {
|
|
124
|
+
installRegistry({ minCliVersion: '9.2.1' });
|
|
125
|
+
const report = await (0, checks_1.preflight)(make({
|
|
126
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
127
|
+
}));
|
|
128
|
+
expect(report.checks.map(c => c.id)).not.toContain('context-kernel');
|
|
129
|
+
});
|
|
130
|
+
it('blocks preflight when an active registry declares an invalid context kernel schema', async () => {
|
|
131
|
+
installRegistry({ projectContextSchema: 2 });
|
|
132
|
+
const report = await (0, checks_1.preflight)(make({
|
|
133
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
134
|
+
}));
|
|
135
|
+
expect(report.status).toBe('degraded');
|
|
136
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
137
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
138
|
+
ok: false,
|
|
139
|
+
advisory: false,
|
|
140
|
+
detail: expect.stringMatching(/projectContextSchema.*exactly 1.*2/),
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
it('fails closed when an earlier active registry manifest is malformed before a valid declaration', async () => {
|
|
144
|
+
installRegistry({ projectContextSchema: 2 }, 'malformed');
|
|
145
|
+
installRegistry({ projectContextSchema: 1 }, 'valid');
|
|
146
|
+
const report = await (0, checks_1.preflight)(make({
|
|
147
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
148
|
+
}));
|
|
149
|
+
expect(report.status).toBe('degraded');
|
|
150
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
151
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
152
|
+
ok: false,
|
|
153
|
+
advisory: false,
|
|
154
|
+
detail: expect.stringMatching(/malformed.*awm-registry\.json.*projectContextSchema.*exactly 1/i),
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
it('keeps legacy ready but renders a persistent warning and remedy', async () => {
|
|
158
|
+
installRegistry({ projectContextSchema: 1 });
|
|
159
|
+
const report = await (0, checks_1.preflight)(make({
|
|
160
|
+
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
161
|
+
}));
|
|
162
|
+
expect(report.status).toBe('ready');
|
|
163
|
+
expect(report.checks).toContainEqual(expect.objectContaining({
|
|
164
|
+
id: 'context-kernel', ok: false, advisory: true,
|
|
165
|
+
detail: expect.stringMatching(/legacy full context/),
|
|
166
|
+
remedy: expect.stringMatching(/project-context-init/),
|
|
167
|
+
}));
|
|
168
|
+
expect((0, preflight_1.formatReport)(report)).toMatch(/⚠.*context-kernel.*legacy full context/s);
|
|
169
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
|
|
170
|
+
expect(JSON.parse(JSON.stringify(report))).toEqual(expect.objectContaining({
|
|
171
|
+
status: 'ready',
|
|
172
|
+
checks: expect.arrayContaining([expect.objectContaining({
|
|
173
|
+
id: 'context-kernel', ok: false, advisory: true,
|
|
174
|
+
detail: expect.any(String), remedy: expect.any(String),
|
|
175
|
+
})]),
|
|
176
|
+
}));
|
|
177
|
+
});
|
|
178
|
+
it('keeps an unconfigured legacy project not_configured while the context kernel remains advisory', async () => {
|
|
179
|
+
installRegistry({ projectContextSchema: 1 });
|
|
180
|
+
const report = await (0, checks_1.preflight)(make());
|
|
181
|
+
expect(report.status).toBe('not_configured');
|
|
182
|
+
expect(check(report, 'manifest')).toMatchObject({ ok: false, detail: 'no .awm/sensors.json' });
|
|
183
|
+
expect(check(report, 'context-kernel')).toMatchObject({
|
|
184
|
+
ok: false,
|
|
185
|
+
advisory: true,
|
|
186
|
+
detail: expect.stringMatching(/legacy full context/),
|
|
187
|
+
});
|
|
188
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
189
|
+
});
|
|
190
|
+
it('passes a valid kernel', async () => {
|
|
191
|
+
installRegistry({ projectContextSchema: 1 });
|
|
192
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
193
|
+
writeValidKernel(dir);
|
|
194
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
195
|
+
expect(report).toEqual(expect.objectContaining({ status: 'ready' }));
|
|
196
|
+
expect(check(report, 'context-kernel')).toMatchObject({ ok: true });
|
|
197
|
+
expect(check(report, 'context-kernel')).not.toHaveProperty('advisory');
|
|
198
|
+
});
|
|
199
|
+
it('degrades partial or invalid migration', async () => {
|
|
200
|
+
installRegistry({ projectContextSchema: 1 });
|
|
201
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
202
|
+
writePartialKernel(dir);
|
|
203
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
204
|
+
expect(report.status).toBe('degraded');
|
|
205
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
206
|
+
expect(report.checks).toContainEqual(expect.objectContaining({
|
|
207
|
+
id: 'context-kernel', advisory: false, ok: false,
|
|
208
|
+
}));
|
|
209
|
+
expect((0, preflight_1.formatReport)(report)).toMatch(/Harness degraded — it has blocking preflight failures\./);
|
|
210
|
+
expect((0, preflight_1.formatReport)(report)).not.toMatch(/declares sensors it cannot run/);
|
|
211
|
+
});
|
|
212
|
+
it('degrades an invalid kernel even when no sensor manifest exists', async () => {
|
|
213
|
+
installRegistry({ projectContextSchema: 1 });
|
|
214
|
+
const dir = make();
|
|
215
|
+
writePartialKernel(dir);
|
|
216
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
217
|
+
expect(report.status).toBe('degraded');
|
|
218
|
+
expect(check(report, 'manifest')).toMatchObject({ ok: false, detail: 'no .awm/sensors.json' });
|
|
219
|
+
expect(check(report, 'context-kernel')).toMatchObject({ ok: false, advisory: false });
|
|
220
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
221
|
+
});
|
|
57
222
|
it('keeps default preflight static and does not dispatch sensors', async () => {
|
|
58
223
|
const dir = make({
|
|
59
224
|
manifest: { pack: 'generic', sensors: { security: { enabled: false } } },
|
|
60
225
|
});
|
|
61
226
|
await (0, checks_1.preflight)(dir);
|
|
62
227
|
expect(mockRunSensors).not.toHaveBeenCalled();
|
|
228
|
+
expect(mockCheckCurrentness).not.toHaveBeenCalled();
|
|
229
|
+
});
|
|
230
|
+
it('adds authoritative currentness only when strict mode is explicitly requested', async () => {
|
|
231
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
232
|
+
const report = await (0, checks_1.preflight)(dir, { requireCurrent: true });
|
|
233
|
+
expect(mockCheckCurrentness).toHaveBeenCalledWith(dir);
|
|
234
|
+
expect(report).toMatchObject({
|
|
235
|
+
status: 'ready',
|
|
236
|
+
currentness: expect.objectContaining({
|
|
237
|
+
components: [expect.objectContaining({ component: 'cli', status: 'current' })],
|
|
238
|
+
}),
|
|
239
|
+
});
|
|
240
|
+
expect(report.checks).toContainEqual(expect.objectContaining({ id: 'currentness', ok: true }));
|
|
241
|
+
expect((0, preflight_1.formatReport)(report)).toContain('Currentness: current');
|
|
242
|
+
expect(JSON.parse(JSON.stringify(report))).toHaveProperty('currentness.components');
|
|
243
|
+
});
|
|
244
|
+
it('returns a complete strict report instead of throwing for a malformed local registry inventory', async () => {
|
|
245
|
+
fs_1.default.writeFileSync(path_1.default.join(process.env.AWM_HOME, 'registries.json'), '{not-json');
|
|
246
|
+
mockCheckCurrentness.mockResolvedValue({
|
|
247
|
+
checkedAt: '2026-08-26T00:00:00.000Z', compatibility: { status: 'not-checked' },
|
|
248
|
+
components: [{
|
|
249
|
+
component: 'registry:inventory', installed: null, latest: null, channel: 'stable',
|
|
250
|
+
source: '[configured registry inventory]', checkedAt: '2026-08-26T00:00:00.000Z', status: 'unverifiable',
|
|
251
|
+
detail: 'Authoritative currentness could not be verified.',
|
|
252
|
+
remedy: 'Repair the local registry inventory and rerun strict preflight.',
|
|
253
|
+
}],
|
|
254
|
+
});
|
|
255
|
+
const report = await (0, checks_1.preflight)(make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } }), { requireCurrent: true });
|
|
256
|
+
expect(report.status).toBe('degraded');
|
|
257
|
+
expect(report.currentness?.components).toEqual(expect.arrayContaining([
|
|
258
|
+
expect.objectContaining({ component: 'registry:inventory', status: 'unverifiable', remedy: expect.stringMatching(/Repair the local registry inventory/) }),
|
|
259
|
+
]));
|
|
260
|
+
expect(JSON.parse(JSON.stringify(report))).toHaveProperty('currentness.components');
|
|
261
|
+
});
|
|
262
|
+
it.each(['stale', 'pinned-behind', 'unverifiable'])('fails strict preflight for a %s component with its exact remedy', async (status) => {
|
|
263
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
264
|
+
mockCheckCurrentness.mockResolvedValue({
|
|
265
|
+
checkedAt: '2026-08-26T00:00:00.000Z', compatibility: { status: 'not-checked' },
|
|
266
|
+
components: [{
|
|
267
|
+
component: 'registry:baseline', installed: 'v1.0.0', latest: 'v1.1.0', channel: 'stable',
|
|
268
|
+
source: 'https://example.test/baseline.git', checkedAt: '2026-08-26T00:00:00.000Z', status,
|
|
269
|
+
detail: 'Authoritative currentness failed.',
|
|
270
|
+
remedy: status === 'pinned-behind' ? 'awm unpin baseline && awm update --yes' : 'awm update --yes',
|
|
271
|
+
}],
|
|
272
|
+
});
|
|
273
|
+
const report = await (0, checks_1.preflight)(dir, { requireCurrent: true });
|
|
274
|
+
expect(report.status).toBe('degraded');
|
|
275
|
+
expect((0, preflight_1.exitCodeFor)(report)).toBe(1);
|
|
276
|
+
expect(report.checks).toContainEqual(expect.objectContaining({ id: 'currentness', ok: false }));
|
|
277
|
+
expect((0, preflight_1.formatReport)(report)).toContain(status === 'pinned-behind' ? 'awm unpin baseline && awm update --yes' : 'awm update --yes');
|
|
278
|
+
});
|
|
279
|
+
it('keeps compatibility failure distinct when authoritative currentness is current', async () => {
|
|
280
|
+
installRegistry({ minCliVersion: '999.0.0' }, 'requires-new-cli');
|
|
281
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
282
|
+
const report = await (0, checks_1.preflight)(dir, { requireCurrent: true });
|
|
283
|
+
expect(report.status).toBe('degraded');
|
|
284
|
+
expect(report.checks).toEqual(expect.arrayContaining([
|
|
285
|
+
expect.objectContaining({ id: 'compatibility', ok: false, detail: expect.stringContaining('requires-new-cli requires CLI >= 999.0.0') }),
|
|
286
|
+
expect.objectContaining({ id: 'currentness', ok: true }),
|
|
287
|
+
]));
|
|
288
|
+
});
|
|
289
|
+
it('retains both empirical sensor and currentness verdicts when both gates are requested', async () => {
|
|
290
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
291
|
+
mockRunSensors.mockResolvedValue({ overall: 'pass', sensors: [] });
|
|
292
|
+
const report = await (0, checks_1.preflight)(dir, { verifySensors: true, requireCurrent: true });
|
|
293
|
+
expect(mockRunSensors).toHaveBeenCalledWith({ cwd: dir, all: true });
|
|
294
|
+
expect(mockCheckCurrentness).toHaveBeenCalledWith(dir);
|
|
295
|
+
expect(report.checks).toEqual(expect.arrayContaining([
|
|
296
|
+
expect.objectContaining({ id: 'sensors-execution', ok: true }),
|
|
297
|
+
expect.objectContaining({ id: 'currentness', ok: true }),
|
|
298
|
+
]));
|
|
63
299
|
});
|
|
64
300
|
it('rejects an array passed as public preflight options before filesystem work', async () => {
|
|
65
301
|
await expect((0, checks_1.preflight)(process.cwd(), []))
|
|
66
|
-
.rejects.toThrow('preflight options must contain
|
|
302
|
+
.rejects.toThrow('preflight options must contain optional boolean verifySensors and requireCurrent');
|
|
67
303
|
});
|
|
68
304
|
it('requires an empirical sensor pass when verification is requested', async () => {
|
|
69
305
|
const dir = make({
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const os_1 = __importDefault(require("os"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
describe('Context Kernel v1 inspection', () => {
|
|
10
|
+
let root;
|
|
11
|
+
let previousAwmHome;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
jest.resetModules();
|
|
14
|
+
previousAwmHome = process.env.AWM_HOME;
|
|
15
|
+
root = fs_1.default.realpathSync(fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-context-kernel-')));
|
|
16
|
+
process.env.AWM_HOME = path_1.default.join(root, 'awm-home');
|
|
17
|
+
fs_1.default.mkdirSync(process.env.AWM_HOME, { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
if (previousAwmHome === undefined)
|
|
21
|
+
delete process.env.AWM_HOME;
|
|
22
|
+
else
|
|
23
|
+
process.env.AWM_HOME = previousAwmHome;
|
|
24
|
+
fs_1.default.rmSync(root, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
function inspect() {
|
|
27
|
+
return require('../../../src/core/context-kernel/inspect').inspectContextKernel;
|
|
28
|
+
}
|
|
29
|
+
function indexPath() {
|
|
30
|
+
return path_1.default.join(root, '.awm', 'context', 'index.json');
|
|
31
|
+
}
|
|
32
|
+
function writeIndex(index) {
|
|
33
|
+
fs_1.default.mkdirSync(path_1.default.dirname(indexPath()), { recursive: true });
|
|
34
|
+
fs_1.default.writeFileSync(indexPath(), JSON.stringify(index, null, 2));
|
|
35
|
+
}
|
|
36
|
+
function writeValidKernel() {
|
|
37
|
+
const index = {
|
|
38
|
+
schema: 1,
|
|
39
|
+
kernelFiles: ['AGENTS.md', 'CONSTITUTION.md'],
|
|
40
|
+
maxFixedBytes: 33_740,
|
|
41
|
+
entries: [
|
|
42
|
+
{ id: 'CTX-PROCESS-001', tier: 'kernel', path: 'CONSTITUTION.md', anchor: 'awm-context:CTX-PROCESS-001', when: 'always' },
|
|
43
|
+
{ id: 'CTX-RELEASE-001', tier: 'selective', path: 'docs/awm/context/releases.md', anchor: 'awm-context:CTX-RELEASE-001', when: 'release automation' },
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
47
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'CONSTITUTION.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\n<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules\n<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
48
|
+
const card = path_1.default.join(root, 'docs', 'awm', 'context', 'releases.md');
|
|
49
|
+
fs_1.default.mkdirSync(path_1.default.dirname(card), { recursive: true });
|
|
50
|
+
fs_1.default.writeFileSync(card, '<!-- awm-context:CTX-RELEASE-001 -->\nrelease rules\n');
|
|
51
|
+
writeIndex(index);
|
|
52
|
+
return index;
|
|
53
|
+
}
|
|
54
|
+
function mutateFixture(mutation) {
|
|
55
|
+
const constitution = path_1.default.join(root, 'CONSTITUTION.md');
|
|
56
|
+
if (mutation === 'start marker missing') {
|
|
57
|
+
fs_1.default.writeFileSync(constitution, fs_1.default.readFileSync(constitution, 'utf8').replace('<!-- AWM:CONTEXT-KERNEL:START v1 -->\n', ''));
|
|
58
|
+
}
|
|
59
|
+
else if (mutation === 'end marker duplicated') {
|
|
60
|
+
fs_1.default.appendFileSync(constitution, '<!-- AWM:CONTEXT-KERNEL:END v1 -->\n');
|
|
61
|
+
}
|
|
62
|
+
else if (mutation === 'anchor outside region') {
|
|
63
|
+
fs_1.default.writeFileSync(constitution, '<!-- awm-context:CTX-PROCESS-001 -->\n' + fs_1.default.readFileSync(constitution, 'utf8'));
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
fs_1.default.writeFileSync(constitution, fs_1.default.readFileSync(constitution, 'utf8').replace('process rules', '<!-- awm-context:CTX-PROCESS-001 -->\nprocess rules'));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
it('classifies no R3 artifacts as legacy', () => {
|
|
70
|
+
expect(inspect()(root)).toEqual({ state: 'legacy' });
|
|
71
|
+
});
|
|
72
|
+
it('rejects a root context marker when the Context Kernel index is missing', () => {
|
|
73
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- AWM:CONTEXT-KERNEL:START v1 -->\nagent rules\n');
|
|
74
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
75
|
+
state: 'invalid',
|
|
76
|
+
detail: expect.stringContaining('.awm/context/index.json is missing'),
|
|
77
|
+
}));
|
|
78
|
+
});
|
|
79
|
+
it('rejects an orphaned context anchor when the Context Kernel index is missing', () => {
|
|
80
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'AGENTS.md'), '<!-- awm-context:CTX-PROCESS-001 -->\nagent rules\n');
|
|
81
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
82
|
+
state: 'invalid',
|
|
83
|
+
detail: expect.stringContaining('.awm/context/index.json is missing'),
|
|
84
|
+
}));
|
|
85
|
+
});
|
|
86
|
+
it('accepts the canonical fixture', () => {
|
|
87
|
+
writeValidKernel();
|
|
88
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'valid', schema: 1 }));
|
|
89
|
+
});
|
|
90
|
+
it.each([
|
|
91
|
+
['unknown top-level field', (x) => { x.extra = true; }],
|
|
92
|
+
['future schema', (x) => { x.schema = 2; }],
|
|
93
|
+
['duplicate id', (x) => { x.entries[1].id = x.entries[0].id; }],
|
|
94
|
+
['duplicate anchor', (x) => { x.entries[1].anchor = x.entries[0].anchor; }],
|
|
95
|
+
['absolute path', (x) => { x.entries[1].path = '/tmp/outside.md'; }],
|
|
96
|
+
['traversal', (x) => { x.entries[1].path = '../outside.md'; }],
|
|
97
|
+
['empty when', (x) => { x.entries[1].when = ''; }],
|
|
98
|
+
['fixed byte cap', (x) => { x.maxFixedBytes = 1; }],
|
|
99
|
+
])('rejects %s', (_name, mutate) => {
|
|
100
|
+
const index = writeValidKernel();
|
|
101
|
+
mutate(index);
|
|
102
|
+
writeIndex(index);
|
|
103
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
104
|
+
});
|
|
105
|
+
it('rejects a project path that resolves through a symlink to an external regular file', () => {
|
|
106
|
+
const index = writeValidKernel();
|
|
107
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-'));
|
|
108
|
+
try {
|
|
109
|
+
const card = path_1.default.join(root, index.entries[1].path);
|
|
110
|
+
const externalCard = path_1.default.join(outside, 'card.md');
|
|
111
|
+
fs_1.default.writeFileSync(externalCard, '<!-- awm-context:CTX-RELEASE-001 -->\nexternal release rules\n');
|
|
112
|
+
fs_1.default.rmSync(card);
|
|
113
|
+
fs_1.default.symlinkSync(externalCard, card);
|
|
114
|
+
expect(fs_1.default.statSync(card).isFile()).toBe(true);
|
|
115
|
+
expect(fs_1.default.realpathSync(card)).toBe(externalCard);
|
|
116
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({
|
|
117
|
+
state: 'invalid',
|
|
118
|
+
detail: expect.stringContaining('resolves outside the project root'),
|
|
119
|
+
}));
|
|
120
|
+
}
|
|
121
|
+
finally {
|
|
122
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
it('rejects a dangling index symlink instead of treating it as legacy', () => {
|
|
126
|
+
const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-outside-'));
|
|
127
|
+
try {
|
|
128
|
+
fs_1.default.mkdirSync(path_1.default.dirname(indexPath()), { recursive: true });
|
|
129
|
+
fs_1.default.symlinkSync(path_1.default.join(outside, 'index.json'), indexPath());
|
|
130
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
fs_1.default.rmSync(outside, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
it.each(['start marker missing', 'end marker duplicated', 'anchor outside region', 'anchor duplicated'])('rejects %s', (mutation) => {
|
|
137
|
+
writeValidKernel();
|
|
138
|
+
mutateFixture(mutation);
|
|
139
|
+
expect(inspect()(root)).toEqual(expect.objectContaining({ state: 'invalid' }));
|
|
140
|
+
});
|
|
141
|
+
it('throws for an invalid public cwd', () => {
|
|
142
|
+
expect(() => inspect()(path_1.default.join(root, 'missing'))).toThrow(/cwd.*directory/);
|
|
143
|
+
});
|
|
144
|
+
});
|