agentic-workflow-manager 9.16.1 → 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/preflight/checks.js +63 -13
- 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/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- 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/core/init/steps.js +5 -1
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/preflight/preflight.test.js +121 -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/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -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/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -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,261 @@
|
|
|
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 commander_1 = require("commander");
|
|
10
|
+
const env_1 = require("../../../src/commands/env");
|
|
11
|
+
const store_1 = require("../../../src/core/environments/store");
|
|
12
|
+
const catalog = {
|
|
13
|
+
schemaVersion: 1,
|
|
14
|
+
facts: {
|
|
15
|
+
'repos.root': {
|
|
16
|
+
valueType: 'path',
|
|
17
|
+
resolution: 'prompt',
|
|
18
|
+
durability: 'durable',
|
|
19
|
+
inject: 'reference',
|
|
20
|
+
},
|
|
21
|
+
'aws.profile': {
|
|
22
|
+
valueType: 'string',
|
|
23
|
+
resolution: 'prompt',
|
|
24
|
+
durability: 'durable',
|
|
25
|
+
inject: 'reference',
|
|
26
|
+
},
|
|
27
|
+
'env.kind': {
|
|
28
|
+
valueType: 'enum',
|
|
29
|
+
resolution: 'prompt',
|
|
30
|
+
durability: 'durable',
|
|
31
|
+
inject: 'reference',
|
|
32
|
+
values: ['dev', 'prod'],
|
|
33
|
+
},
|
|
34
|
+
'aws.token': {
|
|
35
|
+
valueType: 'string',
|
|
36
|
+
resolution: 'prompt',
|
|
37
|
+
durability: 'volatile',
|
|
38
|
+
inject: 'reference',
|
|
39
|
+
ttl: 1,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
function isolateHome() {
|
|
44
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-env-cmd-'));
|
|
45
|
+
process.env.AWM_HOME = path_1.default.join(tmp, '.awm');
|
|
46
|
+
process.env.HOME = tmp;
|
|
47
|
+
return tmp;
|
|
48
|
+
}
|
|
49
|
+
async function run(argv, loadCatalog = () => catalog, extraDeps = {}) {
|
|
50
|
+
const prog = new commander_1.Command();
|
|
51
|
+
prog.exitOverride();
|
|
52
|
+
prog.configureOutput({ writeErr: () => undefined });
|
|
53
|
+
(0, env_1.registerEnvCommand)(prog, {
|
|
54
|
+
loadCatalog,
|
|
55
|
+
consultedRoots: () => ['/tmp/test-registry'],
|
|
56
|
+
stdinIsTTY: () => false,
|
|
57
|
+
...extraDeps,
|
|
58
|
+
});
|
|
59
|
+
const outSpy = jest.spyOn(process.stdout, 'write').mockImplementation(() => true);
|
|
60
|
+
const errSpy = jest.spyOn(process.stderr, 'write').mockImplementation(() => true);
|
|
61
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
62
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
63
|
+
try {
|
|
64
|
+
await prog.parseAsync(['node', 'awm', 'env', ...argv]);
|
|
65
|
+
}
|
|
66
|
+
finally {
|
|
67
|
+
// keep spies until we collect
|
|
68
|
+
}
|
|
69
|
+
const out = outSpy.mock.calls.map((c) => String(c[0])).join('') +
|
|
70
|
+
logSpy.mock.calls.map((c) => c.map(String).join(' ')).join('\n');
|
|
71
|
+
const err = errSpy.mock.calls.map((c) => String(c[0])).join('') +
|
|
72
|
+
errorSpy.mock.calls.map((c) => c.map(String).join(' ')).join('\n');
|
|
73
|
+
outSpy.mockRestore();
|
|
74
|
+
errSpy.mockRestore();
|
|
75
|
+
logSpy.mockRestore();
|
|
76
|
+
errorSpy.mockRestore();
|
|
77
|
+
return { out, err };
|
|
78
|
+
}
|
|
79
|
+
describe('awm env commands (RF-2, RF-3.3)', () => {
|
|
80
|
+
let tmp;
|
|
81
|
+
let prevHome;
|
|
82
|
+
let prevAwm;
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
prevHome = process.env.HOME;
|
|
85
|
+
prevAwm = process.env.AWM_HOME;
|
|
86
|
+
tmp = isolateHome();
|
|
87
|
+
});
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
90
|
+
if (prevHome === undefined)
|
|
91
|
+
delete process.env.HOME;
|
|
92
|
+
else
|
|
93
|
+
process.env.HOME = prevHome;
|
|
94
|
+
if (prevAwm === undefined)
|
|
95
|
+
delete process.env.AWM_HOME;
|
|
96
|
+
else
|
|
97
|
+
process.env.AWM_HOME = prevAwm;
|
|
98
|
+
});
|
|
99
|
+
it('set --value writes under environments/default/values.json without shelling out (RF-2.1, RF-3.3)', async () => {
|
|
100
|
+
const spawn = jest.spyOn(require('child_process'), 'spawnSync');
|
|
101
|
+
const execFile = jest.spyOn(require('child_process'), 'execFileSync');
|
|
102
|
+
await run(['set', 'repos.root', '--value', '/machine/repos']);
|
|
103
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/machine/repos');
|
|
104
|
+
expect(fs_1.default.existsSync((0, store_1.valuesPath)('default'))).toBe(true);
|
|
105
|
+
expect((0, store_1.valuesPath)('default')).toContain(path_1.default.join('.awm', 'environments', 'default', 'values.json'));
|
|
106
|
+
expect(spawn).not.toHaveBeenCalled();
|
|
107
|
+
expect(execFile).not.toHaveBeenCalled();
|
|
108
|
+
spawn.mockRestore();
|
|
109
|
+
execFile.mockRestore();
|
|
110
|
+
});
|
|
111
|
+
it('status reports missing for unset facts and fresh after set (RF-2.5)', async () => {
|
|
112
|
+
let { out } = await run(['status']);
|
|
113
|
+
expect(out).toMatch(/repos\.root:\s*missing/);
|
|
114
|
+
expect(out).toMatch(/aws\.profile:\s*missing/);
|
|
115
|
+
await run(['set', 'repos.root', '--value', '/r']);
|
|
116
|
+
({ out } = await run(['status']));
|
|
117
|
+
expect(out).toMatch(/repos\.root:\s*fresh/);
|
|
118
|
+
// Unset sibling stays missing — no silent default invent for other ids.
|
|
119
|
+
expect(out).toMatch(/aws\.profile:\s*missing/);
|
|
120
|
+
expect(out).not.toMatch(/aws\.profile:\s*fresh/);
|
|
121
|
+
});
|
|
122
|
+
it('status reports stale for expired volatile fact (RF-2.5)', async () => {
|
|
123
|
+
(0, store_1.writeFact)('default', 'aws.token', {
|
|
124
|
+
value: 'tok',
|
|
125
|
+
status: 'fresh',
|
|
126
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
127
|
+
}, { definition: catalog.facts['aws.token'] });
|
|
128
|
+
const { out } = await run(['status'], () => catalog, {
|
|
129
|
+
now: () => new Date('2026-10-12T00:00:02.000Z'),
|
|
130
|
+
});
|
|
131
|
+
expect(out).toMatch(/aws\.token:\s*stale/);
|
|
132
|
+
});
|
|
133
|
+
it('edit prints absolute store path', async () => {
|
|
134
|
+
const { out } = await run(['edit']);
|
|
135
|
+
expect(out).toContain((0, store_1.valuesPath)((0, store_1.getActiveEnvironmentName)()));
|
|
136
|
+
expect(path_1.default.isAbsolute(out.trim().split(/\s+/).find((t) => t.includes('values.json')) || (0, store_1.valuesPath)('default'))).toBe(true);
|
|
137
|
+
});
|
|
138
|
+
it('refresh --value updates an existing fact without shell (RF-3.3)', async () => {
|
|
139
|
+
await run(['set', 'aws.profile', '--value', 'old']);
|
|
140
|
+
await run(['refresh', 'aws.profile', '--value', 'new-profile']);
|
|
141
|
+
expect((0, store_1.readValues)('default')['aws.profile'].value).toBe('new-profile');
|
|
142
|
+
});
|
|
143
|
+
it('refresh without --value in non-interactive mode errors', async () => {
|
|
144
|
+
await run(['set', 'aws.profile', '--value', 'old']);
|
|
145
|
+
await expect(run(['refresh', 'aws.profile'])).rejects.toThrow(/--value|value/i);
|
|
146
|
+
});
|
|
147
|
+
it('use switches active environment (RF-2.2)', async () => {
|
|
148
|
+
await run(['use', 'staging']);
|
|
149
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('staging');
|
|
150
|
+
await run(['set', 'repos.root', '--value', '/staging/repos']);
|
|
151
|
+
expect((0, store_1.valuesPath)('staging')).toContain(`${path_1.default.sep}staging${path_1.default.sep}values.json`);
|
|
152
|
+
expect((0, store_1.readValues)('staging')['repos.root'].value).toBe('/staging/repos');
|
|
153
|
+
});
|
|
154
|
+
it('set unknown id errors listing consulted roots', async () => {
|
|
155
|
+
await expect(run(['set', 'no.such.fact', '--value', 'x'])).rejects.toThrow(/no\.such\.fact/);
|
|
156
|
+
await expect(run(['set', 'no.such.fact', '--value', 'x'])).rejects.toThrow(/test-registry/);
|
|
157
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
158
|
+
});
|
|
159
|
+
it('set rejects empty path and leaves store unchanged', async () => {
|
|
160
|
+
await expect(run(['set', 'repos.root', '--value', ''])).rejects.toThrow(/path|empty|value/i);
|
|
161
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
162
|
+
});
|
|
163
|
+
it('set rejects invalid enum value', async () => {
|
|
164
|
+
await expect(run(['set', 'env.kind', '--value', 'qa'])).rejects.toThrow(/enum|dev|prod/i);
|
|
165
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
166
|
+
});
|
|
167
|
+
it('set never imports child_process for resolution (RF-3.3)', () => {
|
|
168
|
+
const src = fs_1.default.readFileSync(path_1.default.join(__dirname, '../../../src/commands/env/index.ts'), 'utf-8');
|
|
169
|
+
expect(src).not.toMatch(/child_process/);
|
|
170
|
+
expect(src).not.toMatch(/\bexecSync\b|\bspawnSync\b|\bexecFileSync\b/);
|
|
171
|
+
});
|
|
172
|
+
it('set without --value on TTY uses promptFn (RF-3.3)', async () => {
|
|
173
|
+
const prompts = [];
|
|
174
|
+
await run(['set', 'repos.root'], () => catalog, {
|
|
175
|
+
stdinIsTTY: () => true,
|
|
176
|
+
promptValue: (q) => {
|
|
177
|
+
prompts.push(q);
|
|
178
|
+
return '/prompted/repos';
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
expect(prompts.length).toBeGreaterThanOrEqual(1);
|
|
182
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/prompted/repos');
|
|
183
|
+
});
|
|
184
|
+
it('set without --value when not TTY keeps requireValue error (RF-3.3)', async () => {
|
|
185
|
+
await expect(run(['set', 'repos.root'], () => catalog, { stdinIsTTY: () => false })).rejects.toThrow(/--value|value/i);
|
|
186
|
+
});
|
|
187
|
+
it('refresh --all on TTY prompts each store id via promptFn', async () => {
|
|
188
|
+
await run(['set', 'repos.root', '--value', '/old']);
|
|
189
|
+
await run(['set', 'aws.profile', '--value', 'old-profile']);
|
|
190
|
+
const seen = [];
|
|
191
|
+
const answers = {
|
|
192
|
+
'repos.root': '/new',
|
|
193
|
+
'aws.profile': 'new-profile',
|
|
194
|
+
};
|
|
195
|
+
await run(['refresh', '--all'], () => catalog, {
|
|
196
|
+
stdinIsTTY: () => true,
|
|
197
|
+
promptValue: (q) => {
|
|
198
|
+
const id = Object.keys(answers).find((k) => q.includes(k));
|
|
199
|
+
if (!id)
|
|
200
|
+
throw new Error(`unexpected prompt: ${q}`);
|
|
201
|
+
seen.push(id);
|
|
202
|
+
return answers[id];
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
expect(seen.sort()).toEqual(['aws.profile', 'repos.root']);
|
|
206
|
+
expect((0, store_1.readValues)('default')['repos.root'].value).toBe('/new');
|
|
207
|
+
expect((0, store_1.readValues)('default')['aws.profile'].value).toBe('new-profile');
|
|
208
|
+
});
|
|
209
|
+
it('refresh --all when not TTY keeps clear error', async () => {
|
|
210
|
+
await run(['set', 'repos.root', '--value', '/old']);
|
|
211
|
+
await expect(run(['refresh', '--all'], () => catalog, { stdinIsTTY: () => false })).rejects.toThrow(/--all|interactive|factId|--value|TTY/i);
|
|
212
|
+
});
|
|
213
|
+
it('inject prints buildInjection JSON (default reference)', async () => {
|
|
214
|
+
await run(['set', 'repos.root', '--value', '/r']);
|
|
215
|
+
const { out } = await run(['inject']);
|
|
216
|
+
const parsed = JSON.parse(out.trim());
|
|
217
|
+
expect(parsed.mode).toBe('reference');
|
|
218
|
+
expect(parsed.storePath).toContain('values.json');
|
|
219
|
+
expect(parsed.factIds).toEqual(expect.arrayContaining(['repos.root']));
|
|
220
|
+
expect(parsed.values).toBeUndefined();
|
|
221
|
+
});
|
|
222
|
+
it('inject --mode context includes fresh context values when catalog allows', async () => {
|
|
223
|
+
const contextCatalog = {
|
|
224
|
+
schemaVersion: 1,
|
|
225
|
+
facts: {
|
|
226
|
+
'team.color': {
|
|
227
|
+
valueType: 'string',
|
|
228
|
+
resolution: 'prompt',
|
|
229
|
+
durability: 'durable',
|
|
230
|
+
inject: 'context',
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
await run(['set', 'team.color', '--value', 'blue'], () => contextCatalog);
|
|
235
|
+
const { out } = await run(['inject', '--mode', 'context'], () => contextCatalog);
|
|
236
|
+
const parsed = JSON.parse(out.trim());
|
|
237
|
+
expect(parsed.mode).toBe('context');
|
|
238
|
+
expect(parsed.values).toEqual({ 'team.color': 'blue' });
|
|
239
|
+
});
|
|
240
|
+
it('mergeFactDefinitions refuses to weaken volatile → durable (secret revive)', () => {
|
|
241
|
+
const into = {
|
|
242
|
+
'aws.token': {
|
|
243
|
+
valueType: 'string',
|
|
244
|
+
resolution: 'prompt',
|
|
245
|
+
durability: 'volatile',
|
|
246
|
+
inject: 'context',
|
|
247
|
+
ttl: 60,
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
(0, env_1.mergeFactDefinitions)(into, {
|
|
251
|
+
'aws.token': {
|
|
252
|
+
valueType: 'string',
|
|
253
|
+
resolution: 'prompt',
|
|
254
|
+
durability: 'durable',
|
|
255
|
+
inject: 'context',
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
expect(into['aws.token'].durability).toBe('volatile');
|
|
259
|
+
expect(into['aws.token'].ttl).toBe(60);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -397,6 +397,19 @@ describe('preflight', () => {
|
|
|
397
397
|
expect(execution.remedy).toContain('awm sensors init');
|
|
398
398
|
expect(execution.remedy).not.toContain('named sensor');
|
|
399
399
|
});
|
|
400
|
+
it('does not treat overall=pass as unconditional green when invalidEntries are present', async () => {
|
|
401
|
+
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
402
|
+
mockRunSensors.mockResolvedValue({
|
|
403
|
+
overall: 'pass',
|
|
404
|
+
sensors: [{ name: 'good', status: 'pass', errors: [] }],
|
|
405
|
+
invalidEntries: [{ name: 'broken', reason: 'sensors.broken.formatter is not a registered formatter id' }],
|
|
406
|
+
});
|
|
407
|
+
const report = await (0, checks_1.preflight)(dir, { verifySensors: true });
|
|
408
|
+
const execution = check(report, 'sensors-execution');
|
|
409
|
+
expect(execution.ok).toBe(false);
|
|
410
|
+
expect(execution.detail).toMatch(/invalidEntries|broken/i);
|
|
411
|
+
expect(execution.detail).not.toBe('all selected sensors completed with pass');
|
|
412
|
+
});
|
|
400
413
|
it('reports not_configured when no sensor manifest exists', async () => {
|
|
401
414
|
// The team-rollout case: a developer clones the repo and never runs
|
|
402
415
|
// `awm sensors init`. Today nothing notices until an unattended run is already
|
|
@@ -813,4 +826,112 @@ describe('preflight', () => {
|
|
|
813
826
|
expect(check(report, 'manifest').detail).not.toContain('no .awm/sensors.json');
|
|
814
827
|
expect(check(report, 'tools')).toBeUndefined();
|
|
815
828
|
});
|
|
829
|
+
describe('project-declared diagnostics (S3)', () => {
|
|
830
|
+
it('does not claim not-valid-JSON when JSON parses but a project field fails schema (RF-4.1/4.2)', async () => {
|
|
831
|
+
const dir = make({
|
|
832
|
+
manifest: {
|
|
833
|
+
schemaVersion: 3,
|
|
834
|
+
mode: 'project-sensors',
|
|
835
|
+
sensors: {
|
|
836
|
+
broken: {
|
|
837
|
+
source: 'project',
|
|
838
|
+
enabled: true,
|
|
839
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
840
|
+
formatter: 'not-a-formatter',
|
|
841
|
+
},
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
});
|
|
845
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
846
|
+
const manifestCheck = check(report, 'manifest');
|
|
847
|
+
expect(manifestCheck.ok).toBe(false);
|
|
848
|
+
expect(manifestCheck.detail).not.toBe('.awm/sensors.json is not valid JSON');
|
|
849
|
+
expect(manifestCheck.detail).not.toMatch(/not valid JSON/i);
|
|
850
|
+
expect(manifestCheck.detail).toMatch(/broken/);
|
|
851
|
+
expect(manifestCheck.detail).toMatch(/formatter/);
|
|
852
|
+
expect(report.reason).toMatch(/schema-invalid/i);
|
|
853
|
+
});
|
|
854
|
+
it('still reports not-valid-JSON for truly broken JSON (RF-4.1)', async () => {
|
|
855
|
+
const dir = make({ manifest: '{ not json' });
|
|
856
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
857
|
+
expect(check(report, 'manifest').detail).toContain('not valid JSON');
|
|
858
|
+
});
|
|
859
|
+
it('does not recommend awm sensors init as the first remedy for project schema invalidity (RF-3.3)', async () => {
|
|
860
|
+
const dir = make({
|
|
861
|
+
manifest: {
|
|
862
|
+
schemaVersion: 3,
|
|
863
|
+
mode: 'project-sensors',
|
|
864
|
+
sensors: {
|
|
865
|
+
broken: {
|
|
866
|
+
source: 'project',
|
|
867
|
+
enabled: true,
|
|
868
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt'] },
|
|
869
|
+
formatter: 'not-a-formatter',
|
|
870
|
+
},
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
});
|
|
874
|
+
const remedy = check(await (0, checks_1.preflight)(dir), 'manifest').remedy ?? '';
|
|
875
|
+
const firstLine = remedy.split('\n')[0] ?? remedy;
|
|
876
|
+
expect(firstLine).not.toMatch(/awm sensors init/i);
|
|
877
|
+
// RF-3.3 positive: first remedy names repair of the entry/field, not regen.
|
|
878
|
+
expect(firstLine).toMatch(/repair|entry|field/i);
|
|
879
|
+
});
|
|
880
|
+
it('uses an only-project-honest tools remedy when sensors are empty and pack is null', async () => {
|
|
881
|
+
const dir = make({
|
|
882
|
+
manifest: {
|
|
883
|
+
schemaVersion: 3,
|
|
884
|
+
mode: 'project-sensors',
|
|
885
|
+
sensors: {},
|
|
886
|
+
},
|
|
887
|
+
});
|
|
888
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
889
|
+
const tools = check(report, 'tools');
|
|
890
|
+
expect(tools.ok).toBe(false);
|
|
891
|
+
expect(tools.remedy ?? '').not.toMatch(/['"]null['"]/);
|
|
892
|
+
expect(tools.remedy ?? '').not.toMatch(/awm update/i);
|
|
893
|
+
expect(tools.remedy ?? '').not.toMatch(/registry has no pack/i);
|
|
894
|
+
expect(tools.remedy ?? '').toMatch(/source:"project"|declare a pack/i);
|
|
895
|
+
});
|
|
896
|
+
it('accepts only-project READY status without inventing a pack id (RF-2.8)', async () => {
|
|
897
|
+
const pathDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-preflight-only-project-path-'));
|
|
898
|
+
const previousPath = process.env.PATH;
|
|
899
|
+
try {
|
|
900
|
+
fs_1.default.writeFileSync(path_1.default.join(pathDir, 'terraform'), '#!/bin/sh\nexit 0\n');
|
|
901
|
+
fs_1.default.chmodSync(path_1.default.join(pathDir, 'terraform'), 0o755);
|
|
902
|
+
process.env.PATH = pathDir;
|
|
903
|
+
const dir = make({
|
|
904
|
+
manifest: {
|
|
905
|
+
schemaVersion: 3,
|
|
906
|
+
mode: 'project-sensors',
|
|
907
|
+
sensors: {
|
|
908
|
+
'iac-format': {
|
|
909
|
+
source: 'project',
|
|
910
|
+
enabled: true,
|
|
911
|
+
command: { executable: 'terraform', resolution: 'path', args: ['fmt', '-check'] },
|
|
912
|
+
formatter: 'exit-code',
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
},
|
|
916
|
+
});
|
|
917
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
918
|
+
const manifestCheck = check(report, 'manifest');
|
|
919
|
+
expect(manifestCheck.ok).toBe(true);
|
|
920
|
+
expect(manifestCheck.detail).not.toMatch(/sensor authority is unavailable/i);
|
|
921
|
+
expect(manifestCheck.detail).not.toMatch(/pack ['"]?generic['"]?/i);
|
|
922
|
+
// only-project READY must say so — loose "sensors enabled" alone is pack wording.
|
|
923
|
+
expect(manifestCheck.detail).toMatch(/only-project/i);
|
|
924
|
+
expect(manifestCheck.detail).toMatch(/project-declared/i);
|
|
925
|
+
expect(check(report, 'tools').ok).toBe(true);
|
|
926
|
+
expect(report.mode).toBe('project-sensors');
|
|
927
|
+
}
|
|
928
|
+
finally {
|
|
929
|
+
if (previousPath === undefined)
|
|
930
|
+
delete process.env.PATH;
|
|
931
|
+
else
|
|
932
|
+
process.env.PATH = previousPath;
|
|
933
|
+
fs_1.default.rmSync(pathDir, { recursive: true, force: true });
|
|
934
|
+
}
|
|
935
|
+
});
|
|
936
|
+
});
|
|
816
937
|
});
|