agentic-workflow-manager 9.17.0 → 9.18.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/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/watch/index.js +3 -0
- 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/watch/reopened-196.test.js +9 -2
- 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/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,283 @@
|
|
|
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
|
+
function writeJson(file, value) {
|
|
11
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
12
|
+
fs_1.default.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
13
|
+
}
|
|
14
|
+
function handbook(rows) {
|
|
15
|
+
const body = rows.length === 0 ? '' : rows.map((r) => `| ${r} |`).join('\n') + '\n';
|
|
16
|
+
return `# fixture
|
|
17
|
+
|
|
18
|
+
## Capabilities
|
|
19
|
+
| id | status | notes |
|
|
20
|
+
|----|--------|-------|
|
|
21
|
+
${body}
|
|
22
|
+
## Capability: facts
|
|
23
|
+
Facts chapter.
|
|
24
|
+
`;
|
|
25
|
+
}
|
|
26
|
+
/** Minimal registry that passes structural doctor when facts are unused. */
|
|
27
|
+
function scaffoldBare(root, opts) {
|
|
28
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'skills'), { recursive: true });
|
|
29
|
+
writeJson(path_1.default.join(root, 'awm-registry.json'), {
|
|
30
|
+
overrides: [],
|
|
31
|
+
...(opts?.minCliVersion !== undefined ? { minCliVersion: opts.minCliVersion } : {}),
|
|
32
|
+
});
|
|
33
|
+
const catalogVersion = opts?.catalogVersion ?? '1.0.0';
|
|
34
|
+
if (opts?.withBundle !== false) {
|
|
35
|
+
writeJson(path_1.default.join(root, 'catalog.json'), {
|
|
36
|
+
version: 1,
|
|
37
|
+
bundles: [{ name: 'demo', source: 'bundles/demo', version: catalogVersion, scope: 'project' }],
|
|
38
|
+
});
|
|
39
|
+
writeJson(path_1.default.join(root, 'bundles', 'demo', 'bundle.json'), {
|
|
40
|
+
name: 'demo',
|
|
41
|
+
version: opts?.bundleVersion ?? catalogVersion,
|
|
42
|
+
skills: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
writeJson(path_1.default.join(root, 'catalog.json'), { version: 1, bundles: [] });
|
|
47
|
+
}
|
|
48
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'REGISTRY.md'), opts?.handbook ?? handbook(['facts | active | see chapter']));
|
|
49
|
+
if (opts?.facts !== undefined) {
|
|
50
|
+
writeJson(path_1.default.join(root, 'facts.json'), opts.facts);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
describe('doctorRegistry (RF-4.4–4.8, RF-5.1)', () => {
|
|
54
|
+
let tmp;
|
|
55
|
+
beforeEach(() => {
|
|
56
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-doctor-'));
|
|
57
|
+
jest.resetModules();
|
|
58
|
+
});
|
|
59
|
+
afterEach(() => {
|
|
60
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
61
|
+
});
|
|
62
|
+
function loadDoctor() {
|
|
63
|
+
return require('../../../src/commands/registry/doctor');
|
|
64
|
+
}
|
|
65
|
+
it('reports missing REGISTRY.md as structural', () => {
|
|
66
|
+
const root = path_1.default.join(tmp, 'no-handbook');
|
|
67
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
68
|
+
fs_1.default.unlinkSync(path_1.default.join(root, 'REGISTRY.md'));
|
|
69
|
+
const { doctorRegistry } = loadDoctor();
|
|
70
|
+
const report = doctorRegistry(root);
|
|
71
|
+
expect(report.structural.some((m) => /missing REGISTRY\.md/i.test(m))).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('reports missing Capabilities index as structural', () => {
|
|
74
|
+
const root = path_1.default.join(tmp, 'no-caps');
|
|
75
|
+
scaffoldBare(root, {
|
|
76
|
+
withBundle: false,
|
|
77
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
78
|
+
handbook: '# fixture\n\nNo capabilities table here.\n',
|
|
79
|
+
});
|
|
80
|
+
const { doctorRegistry } = loadDoctor();
|
|
81
|
+
const report = doctorRegistry(root);
|
|
82
|
+
expect(report.structural.some((m) => /Capabilities/i.test(m))).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
it('reports catalog vs bundle.json version skew as structural (RF-4.7)', () => {
|
|
85
|
+
const root = path_1.default.join(tmp, 'skew');
|
|
86
|
+
scaffoldBare(root, {
|
|
87
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
88
|
+
catalogVersion: '1.0.0',
|
|
89
|
+
bundleVersion: '2.0.0',
|
|
90
|
+
});
|
|
91
|
+
const { doctorRegistry } = loadDoctor();
|
|
92
|
+
const report = doctorRegistry(root);
|
|
93
|
+
expect(report.structural.some((m) => /version/i.test(m) && /demo/i.test(m))).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
it('reports environment store files under registry tree as structural (RF-4.5)', () => {
|
|
96
|
+
const root = path_1.default.join(tmp, 'store-in-tree');
|
|
97
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
98
|
+
const store = path_1.default.join(root, 'environments', 'default', 'values.json');
|
|
99
|
+
writeJson(store, { 'repos.root': { value: '/x', status: 'fresh', updatedAt: '2026-01-01T00:00:00Z' } });
|
|
100
|
+
const { doctorRegistry } = loadDoctor();
|
|
101
|
+
const report = doctorRegistry(root);
|
|
102
|
+
expect(report.structural.some((m) => /environments/i.test(m) || /values\.json/i.test(m))).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
it('reports environments symlink into external store as structural (RF-4.5)', () => {
|
|
105
|
+
const root = path_1.default.join(tmp, 'store-symlink');
|
|
106
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
107
|
+
const external = path_1.default.join(tmp, 'external-env');
|
|
108
|
+
writeJson(path_1.default.join(external, 'default', 'values.json'), {
|
|
109
|
+
'repos.root': { value: '/x', status: 'fresh', updatedAt: '2026-01-01T00:00:00Z' },
|
|
110
|
+
});
|
|
111
|
+
fs_1.default.symlinkSync(external, path_1.default.join(root, 'environments'));
|
|
112
|
+
const { doctorRegistry } = loadDoctor();
|
|
113
|
+
const report = doctorRegistry(root);
|
|
114
|
+
expect(report.structural.some((m) => /environments/i.test(m) && /symlink|RF-4\.5/i.test(m))).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it('reports values.json / meta.json symlinks under environments as structural (RF-4.5)', () => {
|
|
117
|
+
const root = path_1.default.join(tmp, 'store-file-symlink');
|
|
118
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
119
|
+
const external = path_1.default.join(tmp, 'external-values.json');
|
|
120
|
+
fs_1.default.writeFileSync(external, '{"x":1}\n');
|
|
121
|
+
fs_1.default.mkdirSync(path_1.default.join(root, 'environments', 'default'), { recursive: true });
|
|
122
|
+
fs_1.default.symlinkSync(external, path_1.default.join(root, 'environments', 'default', 'values.json'));
|
|
123
|
+
const { doctorRegistry } = loadDoctor();
|
|
124
|
+
const report = doctorRegistry(root);
|
|
125
|
+
expect(report.structural.some((m) => /values\.json/i.test(m))).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
it('treats unknown capability id widgets as advisory only; doctorExitCode is 0 (RF-4.4/4.6)', () => {
|
|
128
|
+
const root = path_1.default.join(tmp, 'widgets');
|
|
129
|
+
scaffoldBare(root, {
|
|
130
|
+
withBundle: false,
|
|
131
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
132
|
+
handbook: handbook(['widgets | planned | future']),
|
|
133
|
+
});
|
|
134
|
+
const { doctorRegistry, doctorExitCode } = loadDoctor();
|
|
135
|
+
const report = doctorRegistry(root);
|
|
136
|
+
expect(report.structural).toEqual([]);
|
|
137
|
+
expect(report.advisories.some((m) => /widgets/i.test(m))).toBe(true);
|
|
138
|
+
expect(doctorExitCode(report)).toBe(0);
|
|
139
|
+
expect(doctorExitCode({ structural: ['x'], advisories: [] })).toBe(1);
|
|
140
|
+
});
|
|
141
|
+
it('fails structurally when facts present and minCliVersion below FACTS_MIN_CLI_VERSION (RF-4.8)', () => {
|
|
142
|
+
const root = path_1.default.join(tmp, 'old-cli');
|
|
143
|
+
scaffoldBare(root, {
|
|
144
|
+
withBundle: false,
|
|
145
|
+
minCliVersion: '9.17.0',
|
|
146
|
+
facts: { schemaVersion: 1, facts: {} },
|
|
147
|
+
});
|
|
148
|
+
const { doctorRegistry } = loadDoctor();
|
|
149
|
+
const report = doctorRegistry(root);
|
|
150
|
+
expect(report.structural.some((m) => /minCliVersion/i.test(m) || /9\.18\.0/.test(m))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
it('advises on prose home-path hints like ~/Developments', () => {
|
|
153
|
+
const root = path_1.default.join(tmp, 'prose');
|
|
154
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
155
|
+
const skillDir = path_1.default.join(root, 'skills', 'demo');
|
|
156
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
157
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), `---\nname: demo\ndescription: d\n---\nClone under ~/Developments/team.\n`);
|
|
158
|
+
const { doctorRegistry } = loadDoctor();
|
|
159
|
+
const report = doctorRegistry(root);
|
|
160
|
+
expect(report.advisories.some((m) => /~\/Developments|home path|machine path/i.test(m))).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
it('reports orphan needs as structural when facts.json exists (RF-1.3)', () => {
|
|
163
|
+
const root = path_1.default.join(tmp, 'orphan');
|
|
164
|
+
scaffoldBare(root, {
|
|
165
|
+
withBundle: false,
|
|
166
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
167
|
+
facts: {
|
|
168
|
+
schemaVersion: 1,
|
|
169
|
+
facts: { 'repos.root': { valueType: 'path' } },
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
const skillDir = path_1.default.join(root, 'skills', 'demo');
|
|
173
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
174
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), `---\nname: demo\ndescription: d\nneeds:\n - ghost.fact\n---\n# Demo\n`);
|
|
175
|
+
const { doctorRegistry } = loadDoctor();
|
|
176
|
+
const report = doctorRegistry(root);
|
|
177
|
+
expect(report.structural.some((m) => /ghost\.fact|orphan/i.test(m))).toBe(true);
|
|
178
|
+
});
|
|
179
|
+
it('reports needs without facts.json as structural (fail closed)', () => {
|
|
180
|
+
const root = path_1.default.join(tmp, 'needs-no-catalog');
|
|
181
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
182
|
+
const skillDir = path_1.default.join(root, 'skills', 'demo');
|
|
183
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
184
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), `---\nname: demo\ndescription: d\nneeds:\n - repos.root\n---\n# Demo\n`);
|
|
185
|
+
const { doctorRegistry } = loadDoctor();
|
|
186
|
+
const report = doctorRegistry(root);
|
|
187
|
+
expect(report.structural.some((m) => /needs/i.test(m) && /facts\.json/i.test(m) && /missing/i.test(m))).toBe(true);
|
|
188
|
+
expect(report.structural.every((m) => !/invalid|unreadable/i.test(m) || !/needs/i.test(m))).toBe(true);
|
|
189
|
+
});
|
|
190
|
+
it('reports needs + invalid facts.json as invalid/unreadable, not missing', () => {
|
|
191
|
+
const root = path_1.default.join(tmp, 'needs-bad-catalog');
|
|
192
|
+
scaffoldBare(root, {
|
|
193
|
+
withBundle: false,
|
|
194
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
195
|
+
facts: { schemaVersion: 1, facts: {} },
|
|
196
|
+
});
|
|
197
|
+
fs_1.default.writeFileSync(path_1.default.join(root, 'facts.json'), '{not-json');
|
|
198
|
+
const skillDir = path_1.default.join(root, 'skills', 'demo');
|
|
199
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
200
|
+
fs_1.default.writeFileSync(path_1.default.join(skillDir, 'SKILL.md'), `---\nname: demo\ndescription: d\nneeds:\n - repos.root\n---\n# Demo\n`);
|
|
201
|
+
const { doctorRegistry } = loadDoctor();
|
|
202
|
+
const report = doctorRegistry(root);
|
|
203
|
+
const needsMsg = report.structural.find((m) => /needs/i.test(m) && /facts\.json/i.test(m));
|
|
204
|
+
expect(needsMsg).toBeDefined();
|
|
205
|
+
expect(needsMsg).toMatch(/invalid|unreadable/i);
|
|
206
|
+
expect(needsMsg).not.toMatch(/\bmissing\b/i);
|
|
207
|
+
});
|
|
208
|
+
it('fails structurally when facts present and minCliVersion key absent (RF-4.8)', () => {
|
|
209
|
+
const root = path_1.default.join(tmp, 'facts-no-min');
|
|
210
|
+
scaffoldBare(root, {
|
|
211
|
+
withBundle: false,
|
|
212
|
+
facts: { schemaVersion: 1, facts: {} },
|
|
213
|
+
});
|
|
214
|
+
// scaffoldBare omits minCliVersion when not provided
|
|
215
|
+
const { doctorRegistry } = loadDoctor();
|
|
216
|
+
const report = doctorRegistry(root);
|
|
217
|
+
expect(report.structural.some((m) => /minCliVersion/i.test(m))).toBe(true);
|
|
218
|
+
});
|
|
219
|
+
it('rejects empty / non-string root with structural error before path.resolve', () => {
|
|
220
|
+
const { doctorRegistry } = loadDoctor();
|
|
221
|
+
expect(doctorRegistry('').structural.some((m) => /root|required|non-empty|string/i.test(m))).toBe(true);
|
|
222
|
+
expect(doctorRegistry(' ').structural.some((m) => /root|required|non-empty|string/i.test(m))).toBe(true);
|
|
223
|
+
expect(doctorRegistry(null).structural.some((m) => /root|required|non-empty|string/i.test(m))).toBe(true);
|
|
224
|
+
});
|
|
225
|
+
it('reports symlink skill dirs and non-regular SKILL.md as structural (fail closed)', () => {
|
|
226
|
+
const root = path_1.default.join(tmp, 'skill-symlink');
|
|
227
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
228
|
+
const externalSkill = path_1.default.join(tmp, 'external-skill');
|
|
229
|
+
fs_1.default.mkdirSync(externalSkill, { recursive: true });
|
|
230
|
+
fs_1.default.writeFileSync(path_1.default.join(externalSkill, 'SKILL.md'), '---\nname: linked\ndescription: d\n---\n');
|
|
231
|
+
fs_1.default.symlinkSync(externalSkill, path_1.default.join(root, 'skills', 'linked'));
|
|
232
|
+
const regular = path_1.default.join(root, 'skills', 'regular');
|
|
233
|
+
fs_1.default.mkdirSync(regular, { recursive: true });
|
|
234
|
+
const externalMd = path_1.default.join(tmp, 'external-SKILL.md');
|
|
235
|
+
fs_1.default.writeFileSync(externalMd, '---\nname: regular\ndescription: d\n---\n');
|
|
236
|
+
fs_1.default.symlinkSync(externalMd, path_1.default.join(regular, 'SKILL.md'));
|
|
237
|
+
const { doctorRegistry } = loadDoctor();
|
|
238
|
+
const report = doctorRegistry(root);
|
|
239
|
+
expect(report.structural.some((m) => /linked/i.test(m) && /symlink|skill/i.test(m))).toBe(true);
|
|
240
|
+
expect(report.structural.some((m) => /regular/i.test(m) && /SKILL\.md|symlink|regular file/i.test(m))).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
// chmod(0) is a POSIX permission seam; on win32 NTFS ACLs often still allow
|
|
243
|
+
// the creating user to read, so the unreadable path is not reproducible there.
|
|
244
|
+
(process.platform === 'win32' ? it.skip : it)('reports unreadable SKILL.md as structural (fail closed)', () => {
|
|
245
|
+
const root = path_1.default.join(tmp, 'skill-unreadable');
|
|
246
|
+
scaffoldBare(root, { withBundle: false, minCliVersion: types_1.FACTS_MIN_CLI_VERSION });
|
|
247
|
+
const skillDir = path_1.default.join(root, 'skills', 'locked');
|
|
248
|
+
fs_1.default.mkdirSync(skillDir, { recursive: true });
|
|
249
|
+
const skillMd = path_1.default.join(skillDir, 'SKILL.md');
|
|
250
|
+
fs_1.default.writeFileSync(skillMd, '---\nname: locked\ndescription: d\n---\n');
|
|
251
|
+
fs_1.default.chmodSync(skillMd, 0);
|
|
252
|
+
try {
|
|
253
|
+
const { doctorRegistry } = loadDoctor();
|
|
254
|
+
const report = doctorRegistry(root);
|
|
255
|
+
expect(report.structural.some((m) => /locked|SKILL\.md|Cannot read|unreadable/i.test(m))).toBe(true);
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
fs_1.default.chmodSync(skillMd, 0o644);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
it('does not require facts chapter beyond Capabilities when no facts.json and no needs', () => {
|
|
262
|
+
const root = path_1.default.join(tmp, 'legacy');
|
|
263
|
+
scaffoldBare(root, {
|
|
264
|
+
withBundle: false,
|
|
265
|
+
handbook: handbook(['hooks | n/a | none']),
|
|
266
|
+
});
|
|
267
|
+
const { doctorRegistry } = loadDoctor();
|
|
268
|
+
const report = doctorRegistry(root);
|
|
269
|
+
expect(report.structural).toEqual([]);
|
|
270
|
+
});
|
|
271
|
+
it('doctor API is path-only and does not require writing-skills (RF-5.1)', () => {
|
|
272
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/commands/registry/doctor.ts'), 'utf8');
|
|
273
|
+
expect(source).toMatch(/export function doctorRegistry\(\s*root:\s*string/);
|
|
274
|
+
expect(source).not.toMatch(/writing-skills/);
|
|
275
|
+
expect(source).not.toMatch(/writeSkill|writeFileSync.*SKILL/);
|
|
276
|
+
});
|
|
277
|
+
it('wires awm registry doctor [name|path] via doctorExitCode', () => {
|
|
278
|
+
const indexSource = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/commands/registry/index.ts'), 'utf8');
|
|
279
|
+
expect(indexSource).toMatch(/doctorRegistry/);
|
|
280
|
+
expect(indexSource).toMatch(/doctorExitCode/);
|
|
281
|
+
expect(indexSource).toMatch(/process\.exit\(doctorExitCode\(/);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
@@ -0,0 +1,186 @@
|
|
|
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 child_process_1 = require("child_process");
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const types_1 = require("../../../src/core/facts/types");
|
|
11
|
+
const versioning_1 = require("../../../src/core/versioning");
|
|
12
|
+
const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t -c user.name=t ${cmd}`, { cwd, stdio: 'pipe' });
|
|
13
|
+
describe('scaffoldRegistry (RF-4.1, RF-4.2, RF-4.3)', () => {
|
|
14
|
+
let tmp;
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-scaffold-'));
|
|
17
|
+
jest.resetModules();
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
21
|
+
});
|
|
22
|
+
function loadScaffold() {
|
|
23
|
+
return require('../../../src/commands/registry/scaffold');
|
|
24
|
+
}
|
|
25
|
+
function loadDoctor() {
|
|
26
|
+
return require('../../../src/commands/registry/doctor');
|
|
27
|
+
}
|
|
28
|
+
function loadValidate() {
|
|
29
|
+
return require('../../../src/core/registries');
|
|
30
|
+
}
|
|
31
|
+
it('writes min layout with versions, facts, handbook; doctor structural empty (RF-4.1/4.2)', () => {
|
|
32
|
+
const dir = path_1.default.join(tmp, 'new-reg');
|
|
33
|
+
const { scaffoldRegistry } = loadScaffold();
|
|
34
|
+
const result = scaffoldRegistry(dir);
|
|
35
|
+
expect(result.ok).toBe(true);
|
|
36
|
+
if (!result.ok)
|
|
37
|
+
return;
|
|
38
|
+
const manifest = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'awm-registry.json'), 'utf-8'));
|
|
39
|
+
expect(typeof manifest.minCliVersion).toBe('string');
|
|
40
|
+
expect(manifest.minCliVersion).not.toMatch(/^v/i);
|
|
41
|
+
expect((0, versioning_1.compareSemver)(manifest.minCliVersion, types_1.FACTS_MIN_CLI_VERSION)).toBeGreaterThanOrEqual(0);
|
|
42
|
+
const catalog = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'catalog.json'), 'utf-8'));
|
|
43
|
+
expect(catalog).toEqual({ version: 1, bundles: [] });
|
|
44
|
+
expect(fs_1.default.statSync(path_1.default.join(dir, 'skills')).isDirectory()).toBe(true);
|
|
45
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'skills', '.gitkeep'))).toBe(true);
|
|
46
|
+
const facts = JSON.parse(fs_1.default.readFileSync(path_1.default.join(dir, 'facts.json'), 'utf-8'));
|
|
47
|
+
expect(facts).toEqual({ schemaVersion: 1, facts: {} });
|
|
48
|
+
const handbook = fs_1.default.readFileSync(path_1.default.join(dir, 'REGISTRY.md'), 'utf-8');
|
|
49
|
+
expect(handbook).toMatch(/^##\s+Capabilities\s*$/m);
|
|
50
|
+
expect(handbook).toMatch(/^\|\s*id\s*\|\s*status\s*\|\s*notes\s*\|$/m);
|
|
51
|
+
const capsStart = handbook.search(/^##\s+Capabilities\s*$/m);
|
|
52
|
+
const nextH2 = handbook.slice(capsStart + 1).search(/^##\s+/m);
|
|
53
|
+
const capsWindow = nextH2 === -1
|
|
54
|
+
? handbook.slice(capsStart)
|
|
55
|
+
: handbook.slice(capsStart, capsStart + 1 + nextH2);
|
|
56
|
+
expect(capsWindow).toMatch(/\|\s*facts\s*\|\s*active\s*\|/);
|
|
57
|
+
expect(handbook).toMatch(/^##\s+Capability:\s*facts\s*$/m);
|
|
58
|
+
const { doctorRegistry } = loadDoctor();
|
|
59
|
+
expect(doctorRegistry(dir).structural).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
it('survives git commit+clone: layout valid and doctor structural empty (RF-4.1)', () => {
|
|
62
|
+
const src = path_1.default.join(tmp, 'scaffold-src');
|
|
63
|
+
const { scaffoldRegistry } = loadScaffold();
|
|
64
|
+
const scaffolded = scaffoldRegistry(src);
|
|
65
|
+
expect(scaffolded.ok).toBe(true);
|
|
66
|
+
if (!scaffolded.ok)
|
|
67
|
+
return;
|
|
68
|
+
GIT(src, 'init -q');
|
|
69
|
+
GIT(src, 'add -A');
|
|
70
|
+
GIT(src, 'commit -qm scaffold');
|
|
71
|
+
const clone = path_1.default.join(tmp, 'scaffold-clone');
|
|
72
|
+
(0, child_process_1.execSync)(`git clone -q "${src}" "${clone}"`, { stdio: 'pipe' });
|
|
73
|
+
expect(fs_1.default.existsSync(path_1.default.join(clone, 'skills', '.gitkeep'))).toBe(true);
|
|
74
|
+
const { validateRegistryLayout } = loadValidate();
|
|
75
|
+
expect(validateRegistryLayout(clone)).toBe(true);
|
|
76
|
+
const { doctorRegistry } = loadDoctor();
|
|
77
|
+
expect(doctorRegistry(clone).structural).toEqual([]);
|
|
78
|
+
});
|
|
79
|
+
it('addRegistry accepts local path clone of scaffolded registry (RF-4.1)', async () => {
|
|
80
|
+
const home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-scaffold-add-home-'));
|
|
81
|
+
const prevHome = process.env.HOME;
|
|
82
|
+
const prevAwm = process.env.AWM_HOME;
|
|
83
|
+
process.env.HOME = home;
|
|
84
|
+
process.env.AWM_HOME = path_1.default.join(home, '.awm');
|
|
85
|
+
jest.resetModules();
|
|
86
|
+
try {
|
|
87
|
+
const src = path_1.default.join(tmp, 'scaffold-for-add');
|
|
88
|
+
const { scaffoldRegistry } = require('../../../src/commands/registry/scaffold');
|
|
89
|
+
const scaffolded = scaffoldRegistry(src);
|
|
90
|
+
expect(scaffolded.ok).toBe(true);
|
|
91
|
+
if (!scaffolded.ok)
|
|
92
|
+
return;
|
|
93
|
+
GIT(src, 'init -q');
|
|
94
|
+
GIT(src, 'add -A');
|
|
95
|
+
GIT(src, 'commit -qm scaffold');
|
|
96
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
97
|
+
const { validateRegistryLayout, registryContentRoot } = require('../../../src/core/registries');
|
|
98
|
+
const result = await addRegistry(src, 'scaffold-local');
|
|
99
|
+
expect(result.ok).toBe(true);
|
|
100
|
+
if (!result.ok)
|
|
101
|
+
return;
|
|
102
|
+
expect(validateRegistryLayout(registryContentRoot('scaffold-local'))).toBe(true);
|
|
103
|
+
const { doctorRegistry } = require('../../../src/commands/registry/doctor');
|
|
104
|
+
expect(doctorRegistry(registryContentRoot('scaffold-local')).structural).toEqual([]);
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
fs_1.default.rmSync(home, { recursive: true, force: true });
|
|
108
|
+
if (prevHome === undefined)
|
|
109
|
+
delete process.env.HOME;
|
|
110
|
+
else
|
|
111
|
+
process.env.HOME = prevHome;
|
|
112
|
+
if (prevAwm === undefined)
|
|
113
|
+
delete process.env.AWM_HOME;
|
|
114
|
+
else
|
|
115
|
+
process.env.AWM_HOME = prevAwm;
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
it('refuses non-empty existing dir without writing (RF-4.3)', () => {
|
|
119
|
+
const dir = path_1.default.join(tmp, 'occupied');
|
|
120
|
+
fs_1.default.mkdirSync(dir);
|
|
121
|
+
const sentinel = path_1.default.join(dir, 'keep-me.txt');
|
|
122
|
+
fs_1.default.writeFileSync(sentinel, 'pre-existing\n');
|
|
123
|
+
const { scaffoldRegistry } = loadScaffold();
|
|
124
|
+
const result = scaffoldRegistry(dir);
|
|
125
|
+
expect(result.ok).toBe(false);
|
|
126
|
+
if (result.ok)
|
|
127
|
+
return;
|
|
128
|
+
expect(result.error).toMatch(/exist|non-empty|not empty|overwrite/i);
|
|
129
|
+
expect(fs_1.default.readFileSync(sentinel, 'utf-8')).toBe('pre-existing\n');
|
|
130
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'awm-registry.json'))).toBe(false);
|
|
131
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'REGISTRY.md'))).toBe(false);
|
|
132
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'facts.json'))).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
it('cleans up a directory it created when a mid-write fails (RF-4.3)', () => {
|
|
135
|
+
const dir = path_1.default.join(tmp, 'partial-fail');
|
|
136
|
+
expect(fs_1.default.existsSync(dir)).toBe(false);
|
|
137
|
+
const { scaffoldRegistry } = loadScaffold();
|
|
138
|
+
let writes = 0;
|
|
139
|
+
const result = scaffoldRegistry(dir, {
|
|
140
|
+
writeFileSync: (file, data) => {
|
|
141
|
+
writes += 1;
|
|
142
|
+
if (writes >= 3) {
|
|
143
|
+
throw new Error('injected write failure');
|
|
144
|
+
}
|
|
145
|
+
fs_1.default.writeFileSync(file, data);
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
expect(result.ok).toBe(false);
|
|
149
|
+
if (result.ok)
|
|
150
|
+
return;
|
|
151
|
+
expect(result.error).toContain('injected write failure');
|
|
152
|
+
expect(fs_1.default.existsSync(dir)).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
it('empty preexisting target: mid-fail leaves it empty so retry succeeds (RF-4.3)', () => {
|
|
155
|
+
const dir = path_1.default.join(tmp, 'empty-preexisting');
|
|
156
|
+
fs_1.default.mkdirSync(dir);
|
|
157
|
+
expect(fs_1.default.readdirSync(dir)).toEqual([]);
|
|
158
|
+
const { scaffoldRegistry } = loadScaffold();
|
|
159
|
+
let writes = 0;
|
|
160
|
+
const failed = scaffoldRegistry(dir, {
|
|
161
|
+
writeFileSync: (file, data) => {
|
|
162
|
+
writes += 1;
|
|
163
|
+
if (writes >= 3) {
|
|
164
|
+
throw new Error('injected write failure');
|
|
165
|
+
}
|
|
166
|
+
fs_1.default.writeFileSync(file, data);
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
expect(failed.ok).toBe(false);
|
|
170
|
+
if (failed.ok)
|
|
171
|
+
return;
|
|
172
|
+
expect(failed.error).toContain('injected write failure');
|
|
173
|
+
// Target must not hold partial scaffold files (stage-then-rename).
|
|
174
|
+
if (fs_1.default.existsSync(dir)) {
|
|
175
|
+
expect(fs_1.default.readdirSync(dir)).toEqual([]);
|
|
176
|
+
}
|
|
177
|
+
const retry = scaffoldRegistry(dir);
|
|
178
|
+
expect(retry.ok).toBe(true);
|
|
179
|
+
if (!retry.ok)
|
|
180
|
+
return;
|
|
181
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'awm-registry.json'))).toBe(true);
|
|
182
|
+
expect(fs_1.default.existsSync(path_1.default.join(dir, 'REGISTRY.md'))).toBe(true);
|
|
183
|
+
const { doctorRegistry } = loadDoctor();
|
|
184
|
+
expect(doctorRegistry(dir).structural).toEqual([]);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -921,9 +921,16 @@ describe('issue #196 reopened: supervised S1 handoff', () => {
|
|
|
921
921
|
}
|
|
922
922
|
expect(stdout).toContain('supervisor activo');
|
|
923
923
|
child.kill('SIGINT');
|
|
924
|
-
const exit = await new Promise(resolve =>
|
|
925
|
-
|
|
924
|
+
const exit = await new Promise(resolve => {
|
|
925
|
+
child.once('exit', (code, signal) => resolve({ code, signal }));
|
|
926
|
+
});
|
|
927
|
+
// Contract: truthful interrupt report. Exit status is secondary —
|
|
928
|
+
// on Darwin, child_process often reports code null + SIGINT when the
|
|
929
|
+
// signal disposition races process.exitCode=130 (never use process.exit
|
|
930
|
+
// in watch/index: it kills in-process Jest callers). Either 130 or
|
|
931
|
+
// null+SIGINT is an interrupted stop; require the detenido line either way.
|
|
926
932
|
expect(stdout).toMatch(/detenido; ciclo IN_PROGRESS/);
|
|
933
|
+
expect(exit.code === 130 || (exit.code === null && exit.signal === 'SIGINT')).toBe(true);
|
|
927
934
|
expect(stdout).not.toContain('COMPLETE');
|
|
928
935
|
expect(stderr).not.toContain('Error');
|
|
929
936
|
}
|