agentic-workflow-manager 9.17.0 → 9.19.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/env/index.js +192 -0
- package/dist/src/commands/plan/index.js +1 -1
- package/dist/src/commands/registry/doctor.js +428 -0
- package/dist/src/commands/registry/index.js +52 -0
- package/dist/src/commands/registry/scaffold.js +140 -0
- package/dist/src/commands/sensors/read-only-observation.js +305 -0
- package/dist/src/commands/sensors/run.js +23 -60
- package/dist/src/commands/watch/index.js +3 -0
- package/dist/src/core/admission/index.js +11 -1
- package/dist/src/core/currentness/check.js +5 -2
- package/dist/src/core/environments/resolve.js +122 -0
- package/dist/src/core/environments/store.js +323 -0
- package/dist/src/core/facts/catalog.js +138 -0
- package/dist/src/core/facts/needs.js +57 -0
- package/dist/src/core/facts/types.js +21 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/registry/doctor.test.js +283 -0
- package/dist/tests/commands/registry/scaffold.test.js +186 -0
- package/dist/tests/commands/sensors/read-only-observation.test.js +537 -0
- package/dist/tests/commands/sensors/run-read-only.test.js +151 -0
- package/dist/tests/commands/watch/reopened-196.test.js +9 -2
- package/dist/tests/core/admission/index.test.js +60 -0
- package/dist/tests/core/currentness/check-insteadof.test.js +120 -0
- package/dist/tests/core/currentness/check.test.js +75 -14
- package/dist/tests/core/environments/resolve.test.js +347 -0
- package/dist/tests/core/environments/store.test.js +248 -0
- package/dist/tests/core/facts/catalog.test.js +179 -0
- package/dist/tests/core/facts/needs.test.js +88 -0
- package/dist/tests/structural/cloud-admission-docs-contract.test.js +54 -0
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,179 @@
|
|
|
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
|
+
const types_1 = require("../../../src/core/facts/types");
|
|
10
|
+
const catalog_1 = require("../../../src/core/facts/catalog");
|
|
11
|
+
const validRepos = {
|
|
12
|
+
schemaVersion: 1,
|
|
13
|
+
facts: {
|
|
14
|
+
'repos.root': {
|
|
15
|
+
description: 'Local root for team clones',
|
|
16
|
+
valueType: 'path',
|
|
17
|
+
resolution: 'prompt',
|
|
18
|
+
durability: 'durable',
|
|
19
|
+
inject: 'reference',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
describe('parseFactsCatalog (RF-1.1)', () => {
|
|
24
|
+
it('exports FACTS_MIN_CLI_VERSION as 9.18.0', () => {
|
|
25
|
+
expect(types_1.FACTS_MIN_CLI_VERSION).toBe('9.18.0');
|
|
26
|
+
});
|
|
27
|
+
it('parses a valid schemaVersion 1 catalog with path/prompt/durable/reference', () => {
|
|
28
|
+
const catalog = (0, catalog_1.parseFactsCatalog)(validRepos);
|
|
29
|
+
expect(catalog.schemaVersion).toBe(1);
|
|
30
|
+
expect(catalog.facts['repos.root']).toEqual({
|
|
31
|
+
description: 'Local root for team clones',
|
|
32
|
+
valueType: 'path',
|
|
33
|
+
resolution: 'prompt',
|
|
34
|
+
durability: 'durable',
|
|
35
|
+
inject: 'reference',
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
it('applies defaults durability durable, inject reference, resolution prompt', () => {
|
|
39
|
+
const catalog = (0, catalog_1.parseFactsCatalog)({
|
|
40
|
+
schemaVersion: 1,
|
|
41
|
+
facts: {
|
|
42
|
+
'aws.profile': { valueType: 'string' },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
expect(catalog.facts['aws.profile']).toMatchObject({
|
|
46
|
+
valueType: 'string',
|
|
47
|
+
resolution: 'prompt',
|
|
48
|
+
durability: 'durable',
|
|
49
|
+
inject: 'reference',
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
it('accepts empty facts object', () => {
|
|
53
|
+
expect((0, catalog_1.parseFactsCatalog)({ schemaVersion: 1, facts: {} }).facts).toEqual({});
|
|
54
|
+
});
|
|
55
|
+
it('rejects bad schemaVersion with FactsCatalogError', () => {
|
|
56
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({ schemaVersion: 2, facts: {} })).toThrow(types_1.FactsCatalogError);
|
|
57
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({ schemaVersion: 2, facts: {} })).toThrow(/schemaVersion/);
|
|
58
|
+
});
|
|
59
|
+
it('rejects missing facts object with FactsCatalogError', () => {
|
|
60
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({ schemaVersion: 1 })).toThrow(types_1.FactsCatalogError);
|
|
61
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({ schemaVersion: 1 })).toThrow(/facts/);
|
|
62
|
+
});
|
|
63
|
+
it('rejects unknown valueType with FactsCatalogError', () => {
|
|
64
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
65
|
+
schemaVersion: 1,
|
|
66
|
+
facts: { 'repos.root': { valueType: 'blob' } },
|
|
67
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
68
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
facts: { 'repos.root': { valueType: 'blob' } },
|
|
71
|
+
})).toThrow(/valueType/);
|
|
72
|
+
});
|
|
73
|
+
it('rejects resolution: command with message containing those words and v1 (RF-3.3 catalog)', () => {
|
|
74
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
75
|
+
schemaVersion: 1,
|
|
76
|
+
facts: {
|
|
77
|
+
'repos.root': { valueType: 'path', resolution: 'command' },
|
|
78
|
+
},
|
|
79
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
80
|
+
try {
|
|
81
|
+
(0, catalog_1.parseFactsCatalog)({
|
|
82
|
+
schemaVersion: 1,
|
|
83
|
+
facts: {
|
|
84
|
+
'repos.root': { valueType: 'path', resolution: 'command' },
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
fail('expected throw');
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
const msg = e.message;
|
|
91
|
+
expect(msg).toMatch(/resolution:\s*command/i);
|
|
92
|
+
expect(msg).toMatch(/v1/i);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
it('rejects non-dotted fact ids', () => {
|
|
96
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
97
|
+
schemaVersion: 1,
|
|
98
|
+
facts: { root: { valueType: 'path' } },
|
|
99
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
100
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
101
|
+
schemaVersion: 1,
|
|
102
|
+
facts: { 'Repos.Root': { valueType: 'path' } },
|
|
103
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
104
|
+
});
|
|
105
|
+
it('parses from a file path', () => {
|
|
106
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-facts-'));
|
|
107
|
+
try {
|
|
108
|
+
const file = path_1.default.join(dir, 'facts.json');
|
|
109
|
+
fs_1.default.writeFileSync(file, JSON.stringify(validRepos));
|
|
110
|
+
const catalog = (0, catalog_1.parseFactsCatalog)(file);
|
|
111
|
+
expect(catalog.facts['repos.root'].valueType).toBe('path');
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
it('rejects symlink facts.json (regular-file guard)', () => {
|
|
118
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-facts-'));
|
|
119
|
+
try {
|
|
120
|
+
const real = path_1.default.join(dir, 'real-facts.json');
|
|
121
|
+
fs_1.default.writeFileSync(real, JSON.stringify(validRepos));
|
|
122
|
+
const link = path_1.default.join(dir, 'facts.json');
|
|
123
|
+
fs_1.default.symlinkSync(real, link);
|
|
124
|
+
expect(() => (0, catalog_1.parseFactsCatalog)(link)).toThrow(types_1.FactsCatalogError);
|
|
125
|
+
expect(() => (0, catalog_1.parseFactsCatalog)(link)).toThrow(/symbolic link|regular file/i);
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
it('requires enum values[] when valueType is enum', () => {
|
|
132
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
133
|
+
schemaVersion: 1,
|
|
134
|
+
facts: { 'env.tier': { valueType: 'enum' } },
|
|
135
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
136
|
+
const ok = (0, catalog_1.parseFactsCatalog)({
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
facts: { 'env.tier': { valueType: 'enum', values: ['dev', 'prod'] } },
|
|
139
|
+
});
|
|
140
|
+
expect(ok.facts['env.tier'].values).toEqual(['dev', 'prod']);
|
|
141
|
+
});
|
|
142
|
+
it('rejects volatile without finite ttl in (0, MAX] (RF-2.4 catalog)', () => {
|
|
143
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
144
|
+
schemaVersion: 1,
|
|
145
|
+
facts: { 'aws.token': { valueType: 'string', durability: 'volatile' } },
|
|
146
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
147
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
148
|
+
schemaVersion: 1,
|
|
149
|
+
facts: { 'aws.token': { valueType: 'string', durability: 'volatile', ttl: 0 } },
|
|
150
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
151
|
+
expect(() => (0, catalog_1.parseFactsCatalog)({
|
|
152
|
+
schemaVersion: 1,
|
|
153
|
+
facts: {
|
|
154
|
+
'aws.token': {
|
|
155
|
+
valueType: 'string',
|
|
156
|
+
durability: 'volatile',
|
|
157
|
+
ttl: 365 * 24 * 3600 + 1,
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
})).toThrow(types_1.FactsCatalogError);
|
|
161
|
+
const ok = (0, catalog_1.parseFactsCatalog)({
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
facts: { 'aws.token': { valueType: 'string', durability: 'volatile', ttl: 60 } },
|
|
164
|
+
});
|
|
165
|
+
expect(ok.facts['aws.token'].ttl).toBe(60);
|
|
166
|
+
});
|
|
167
|
+
it('rejects invalid JSON file with named FactsCatalogError (RF-1.1)', () => {
|
|
168
|
+
const dir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-facts-bad-'));
|
|
169
|
+
try {
|
|
170
|
+
const file = path_1.default.join(dir, 'facts.json');
|
|
171
|
+
fs_1.default.writeFileSync(file, '{not-json');
|
|
172
|
+
expect(() => (0, catalog_1.parseFactsCatalog)(file)).toThrow(types_1.FactsCatalogError);
|
|
173
|
+
expect(() => (0, catalog_1.parseFactsCatalog)(file)).toThrow(/Invalid JSON/i);
|
|
174
|
+
}
|
|
175
|
+
finally {
|
|
176
|
+
fs_1.default.rmSync(dir, { recursive: true, force: true });
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
const catalog_1 = require("../../../src/core/facts/catalog");
|
|
10
|
+
const needs_1 = require("../../../src/core/facts/needs");
|
|
11
|
+
const types_1 = require("../../../src/core/facts/types");
|
|
12
|
+
describe('readSkillNeeds / findOrphanNeeds (RF-1.2, RF-1.3, RF-1.4)', () => {
|
|
13
|
+
let tmp;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-needs-'));
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
19
|
+
});
|
|
20
|
+
function writeSkill(body) {
|
|
21
|
+
const dir = path_1.default.join(tmp, 'skills', 'demo');
|
|
22
|
+
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
23
|
+
const skillPath = path_1.default.join(dir, 'SKILL.md');
|
|
24
|
+
fs_1.default.writeFileSync(skillPath, body);
|
|
25
|
+
return skillPath;
|
|
26
|
+
}
|
|
27
|
+
it('resolves needs: [repos.root] from frontmatter (RF-1.2)', () => {
|
|
28
|
+
const skillPath = writeSkill(`---
|
|
29
|
+
name: demo
|
|
30
|
+
description: d
|
|
31
|
+
needs:
|
|
32
|
+
- repos.root
|
|
33
|
+
---
|
|
34
|
+
# Demo
|
|
35
|
+
`);
|
|
36
|
+
expect((0, needs_1.readSkillNeeds)(skillPath)).toEqual(['repos.root']);
|
|
37
|
+
});
|
|
38
|
+
it('accepts flow-style needs list', () => {
|
|
39
|
+
const skillPath = writeSkill(`---
|
|
40
|
+
name: demo
|
|
41
|
+
description: d
|
|
42
|
+
needs: [repos.root, aws.profile.dev]
|
|
43
|
+
---
|
|
44
|
+
# Demo
|
|
45
|
+
`);
|
|
46
|
+
expect((0, needs_1.readSkillNeeds)(skillPath)).toEqual(['repos.root', 'aws.profile.dev']);
|
|
47
|
+
});
|
|
48
|
+
it('returns empty when needs omitted (RF-1.4)', () => {
|
|
49
|
+
const skillPath = writeSkill(`---
|
|
50
|
+
name: demo
|
|
51
|
+
description: d
|
|
52
|
+
---
|
|
53
|
+
# Demo
|
|
54
|
+
`);
|
|
55
|
+
expect((0, needs_1.readSkillNeeds)(skillPath)).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
it('throws SkillNeedsError for invalid needs shape', () => {
|
|
58
|
+
const skillPath = writeSkill(`---
|
|
59
|
+
name: demo
|
|
60
|
+
description: d
|
|
61
|
+
needs: repos.root
|
|
62
|
+
---
|
|
63
|
+
# Demo
|
|
64
|
+
`);
|
|
65
|
+
expect(() => (0, needs_1.readSkillNeeds)(skillPath)).toThrow(types_1.SkillNeedsError);
|
|
66
|
+
});
|
|
67
|
+
it('lists unknown ids as orphans without inventing facts (RF-1.3)', () => {
|
|
68
|
+
const catalog = (0, catalog_1.parseFactsCatalog)({
|
|
69
|
+
schemaVersion: 1,
|
|
70
|
+
facts: { 'repos.root': { valueType: 'path' } },
|
|
71
|
+
});
|
|
72
|
+
expect((0, needs_1.findOrphanNeeds)(['repos.root', 'ghost.fact'], catalog)).toEqual(['ghost.fact']);
|
|
73
|
+
expect((0, needs_1.findOrphanNeeds)(['repos.root'], catalog)).toEqual([]);
|
|
74
|
+
expect((0, needs_1.findOrphanNeeds)([], catalog)).toEqual([]);
|
|
75
|
+
});
|
|
76
|
+
it('treats Object.prototype keys as orphans (not catalog facts)', () => {
|
|
77
|
+
const catalog = (0, catalog_1.parseFactsCatalog)({
|
|
78
|
+
schemaVersion: 1,
|
|
79
|
+
facts: { 'repos.root': { valueType: 'path' } },
|
|
80
|
+
});
|
|
81
|
+
// `in` would match prototype; Object.hasOwn must not.
|
|
82
|
+
expect((0, needs_1.findOrphanNeeds)(['toString'], catalog)).toEqual(['toString']);
|
|
83
|
+
expect((0, needs_1.findOrphanNeeds)(['__proto__', 'constructor'], catalog)).toEqual([
|
|
84
|
+
'__proto__',
|
|
85
|
+
'constructor',
|
|
86
|
+
]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
8
|
+
/** CMD-DOCS — V-R4 (issue #215 documentation). */
|
|
9
|
+
const repoRoot = path_1.default.resolve(__dirname, '../../..');
|
|
10
|
+
/** Whitespace-normalized so re-wrapping the prose never breaks a pinned phrase. */
|
|
11
|
+
const read = (relative) => fs_1.default.readFileSync(path_1.default.join(repoRoot, ...relative.split('/')), 'utf8').replace(/\s+/g, ' ');
|
|
12
|
+
describe('issue #215 documentation contract (V-R4)', () => {
|
|
13
|
+
const configuration = read('docs/configuration.md');
|
|
14
|
+
it('documents .awm/admission.json with defaults, the git rule and the accepted risk', () => {
|
|
15
|
+
expect(configuration).toContain('## Read-only admission observation (`.awm/admission.json`)');
|
|
16
|
+
for (const name of ['node_modules', '.next', '.turbo', 'dist', 'build', 'coverage', '.venv', 'target'])
|
|
17
|
+
expect(configuration).toContain(`\`${name}\``);
|
|
18
|
+
expect(configuration).toContain('git-ignored and contains no tracked file');
|
|
19
|
+
expect(configuration).toContain('**Accepted risk:**');
|
|
20
|
+
expect(configuration).toContain('`admission-config-invalid`');
|
|
21
|
+
expect(configuration).toContain('`readOnlyObservation`');
|
|
22
|
+
expect(configuration).toContain('`awm plan admit` does not print it');
|
|
23
|
+
});
|
|
24
|
+
it.each([
|
|
25
|
+
'250000',
|
|
26
|
+
'1 GiB',
|
|
27
|
+
'`read-only-mutation-detected`',
|
|
28
|
+
'`read-only-observation-unavailable`',
|
|
29
|
+
'`not-ignored`',
|
|
30
|
+
'`contains-tracked`',
|
|
31
|
+
'`git-unavailable`',
|
|
32
|
+
'`.git` or `.awm`',
|
|
33
|
+
'never skipped',
|
|
34
|
+
'including any `.awm` or `.git` nested',
|
|
35
|
+
'regular file',
|
|
36
|
+
'65536 bytes',
|
|
37
|
+
])('documents the observation limit, reason code or reserved-name rule %s', literal => {
|
|
38
|
+
expect(configuration).toContain(literal);
|
|
39
|
+
});
|
|
40
|
+
it('points --verify-sensors in the CLI reference at the observation rules', () => {
|
|
41
|
+
const doc = read('docs/cli-reference.md');
|
|
42
|
+
expect(doc).toContain('configuration.md#read-only-admission-observation-awmadmissionjson');
|
|
43
|
+
expect(doc).toContain('`ADMISSION_SENSORS_BLOCKED`');
|
|
44
|
+
});
|
|
45
|
+
it('states that the D-7 insteadOf recipe passes strict currentness', () => {
|
|
46
|
+
const doc = read('docs/operations/cloud-and-platforms.md');
|
|
47
|
+
expect(doc).toContain('`awm preflight --require-current`');
|
|
48
|
+
expect(doc).toContain('`remote.origin.url`');
|
|
49
|
+
expect(doc).toContain('--local');
|
|
50
|
+
});
|
|
51
|
+
it('describes the snapshot exclusions in the plan admit help', () => {
|
|
52
|
+
expect(read('cli/src/commands/plan/index.ts')).toContain("'require an empirical sensor pass; read-only observation skips git-ignored build output (see .awm/admission.json)'");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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 path_1 = __importDefault(require("path"));
|
|
8
|
+
/**
|
|
9
|
+
* CMD-SCAFFOLD-DOCS — RF-5.2 operator-environments documentation contract.
|
|
10
|
+
* Asserts runbook + cli-reference document scaffold/doctor/env and state that
|
|
11
|
+
* awm-cenco-infra-registry migration is out of scope (RF-5.2).
|
|
12
|
+
*/
|
|
13
|
+
const repoRoot = path_1.default.resolve(__dirname, '../../..');
|
|
14
|
+
const runbookPath = path_1.default.join(repoRoot, 'docs', 'runbook.md');
|
|
15
|
+
const cliReferencePath = path_1.default.join(repoRoot, 'docs', 'cli-reference.md');
|
|
16
|
+
const designPath = 'docs/plans/2026-10-12-operator-environments-design.md';
|
|
17
|
+
function teamRegistriesSection(runbook) {
|
|
18
|
+
const start = runbook.indexOf('## Team registries');
|
|
19
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
20
|
+
const rest = runbook.slice(start);
|
|
21
|
+
const nextH2 = rest.search(/\n## (?!Team)/);
|
|
22
|
+
return nextH2 === -1 ? rest : rest.slice(0, nextH2);
|
|
23
|
+
}
|
|
24
|
+
function registriesSection(cliRef) {
|
|
25
|
+
const start = cliRef.indexOf('## Registries & pinning');
|
|
26
|
+
expect(start).toBeGreaterThanOrEqual(0);
|
|
27
|
+
const rest = cliRef.slice(start);
|
|
28
|
+
const nextH2 = rest.search(/\n## (?!Registries)/);
|
|
29
|
+
return nextH2 === -1 ? rest : rest.slice(0, nextH2);
|
|
30
|
+
}
|
|
31
|
+
describe('operator-environments docs contract (RF-5.2)', () => {
|
|
32
|
+
it('runbook team-registries leads with scaffold and mentions doctor + design RF-5.2 out-of-scope', () => {
|
|
33
|
+
const runbook = fs_1.default.readFileSync(runbookPath, 'utf-8');
|
|
34
|
+
const section = teamRegistriesSection(runbook);
|
|
35
|
+
const scaffoldIdx = section.indexOf('awm registry scaffold');
|
|
36
|
+
const addIdx = section.indexOf('awm registry add');
|
|
37
|
+
expect(scaffoldIdx).toBeGreaterThanOrEqual(0);
|
|
38
|
+
expect(addIdx).toBeGreaterThan(scaffoldIdx);
|
|
39
|
+
expect(section).toContain('awm registry doctor');
|
|
40
|
+
// RF-5.2: Cenco infra registry is out of scope — require the out-of-scope
|
|
41
|
+
// phrase near the cenco mention (same paragraph/window), not a loose
|
|
42
|
+
// match elsewhere in the section.
|
|
43
|
+
expect(section).toMatch(/RF-5\.2/);
|
|
44
|
+
expect(section).toContain(designPath);
|
|
45
|
+
const cencoIdx = section.search(/awm-cenco-infra-registry/);
|
|
46
|
+
expect(cencoIdx).toBeGreaterThanOrEqual(0);
|
|
47
|
+
const windowStart = Math.max(0, cencoIdx - 120);
|
|
48
|
+
const windowEnd = Math.min(section.length, cencoIdx + 160);
|
|
49
|
+
const nearCenco = section.slice(windowStart, windowEnd);
|
|
50
|
+
expect(nearCenco).toMatch(/out of scope|out-of-scope|does not modify|SHALL NOT modify/i);
|
|
51
|
+
});
|
|
52
|
+
it('cli-reference documents registry scaffold, registry doctor, and awm env', () => {
|
|
53
|
+
const cliRef = fs_1.default.readFileSync(cliReferencePath, 'utf-8');
|
|
54
|
+
const registries = registriesSection(cliRef);
|
|
55
|
+
expect(registries).toMatch(/###\s+`awm registry scaffold/);
|
|
56
|
+
expect(registries).toMatch(/###\s+`awm registry doctor/);
|
|
57
|
+
// env lives as its own H2; require the three command strings in the file
|
|
58
|
+
// with scoped checks so headings cannot be satisfied by a stray mention.
|
|
59
|
+
expect(cliRef).toContain('awm registry scaffold');
|
|
60
|
+
expect(cliRef).toContain('awm registry doctor');
|
|
61
|
+
const envStart = cliRef.search(/^##\s+.*\benv\b/im);
|
|
62
|
+
expect(envStart).toBeGreaterThanOrEqual(0);
|
|
63
|
+
const envRest = cliRef.slice(envStart);
|
|
64
|
+
const envNext = envRest.search(/\n## /);
|
|
65
|
+
const envSection = envNext === -1 ? envRest : envRest.slice(0, envNext);
|
|
66
|
+
expect(envSection).toMatch(/awm env/);
|
|
67
|
+
expect(envSection).toMatch(/env set|`awm env set`/i);
|
|
68
|
+
expect(envSection).toMatch(/env status|`awm env status`/i);
|
|
69
|
+
});
|
|
70
|
+
});
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|