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,347 @@
|
|
|
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 resolve_1 = require("../../../src/core/environments/resolve");
|
|
11
|
+
const store_1 = require("../../../src/core/environments/store");
|
|
12
|
+
function isolateHome() {
|
|
13
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-env-resolve-'));
|
|
14
|
+
process.env.AWM_HOME = path_1.default.join(tmp, '.awm');
|
|
15
|
+
process.env.HOME = tmp;
|
|
16
|
+
return tmp;
|
|
17
|
+
}
|
|
18
|
+
const durableCatalog = (0, catalog_1.parseFactsCatalog)({
|
|
19
|
+
schemaVersion: 1,
|
|
20
|
+
facts: {
|
|
21
|
+
'repos.root': {
|
|
22
|
+
valueType: 'path',
|
|
23
|
+
durability: 'durable',
|
|
24
|
+
inject: 'reference',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
const volatileCatalog = (0, catalog_1.parseFactsCatalog)({
|
|
29
|
+
schemaVersion: 1,
|
|
30
|
+
facts: {
|
|
31
|
+
'aws.token': {
|
|
32
|
+
valueType: 'string',
|
|
33
|
+
durability: 'volatile',
|
|
34
|
+
ttl: 1,
|
|
35
|
+
inject: 'reference',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
/** Volatile + inject:context — required so the fresh-status gate in buildInjection is reachable. */
|
|
40
|
+
const volatileContextCatalog = (0, catalog_1.parseFactsCatalog)({
|
|
41
|
+
schemaVersion: 1,
|
|
42
|
+
facts: {
|
|
43
|
+
'aws.token': {
|
|
44
|
+
valueType: 'string',
|
|
45
|
+
durability: 'volatile',
|
|
46
|
+
ttl: 1,
|
|
47
|
+
inject: 'context',
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
const contextCatalog = (0, catalog_1.parseFactsCatalog)({
|
|
52
|
+
schemaVersion: 1,
|
|
53
|
+
facts: {
|
|
54
|
+
'team.color': {
|
|
55
|
+
valueType: 'string',
|
|
56
|
+
durability: 'durable',
|
|
57
|
+
inject: 'context',
|
|
58
|
+
},
|
|
59
|
+
'team.nick': {
|
|
60
|
+
valueType: 'string',
|
|
61
|
+
durability: 'durable',
|
|
62
|
+
inject: 'context',
|
|
63
|
+
},
|
|
64
|
+
'repos.root': {
|
|
65
|
+
valueType: 'path',
|
|
66
|
+
durability: 'durable',
|
|
67
|
+
inject: 'reference',
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
describe('environment resolve / inject (RF-2.3–2.5, RF-3.1–3.2, RNF-2)', () => {
|
|
72
|
+
let tmp;
|
|
73
|
+
let prevHome;
|
|
74
|
+
let prevAwm;
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
prevHome = process.env.HOME;
|
|
77
|
+
prevAwm = process.env.AWM_HOME;
|
|
78
|
+
tmp = isolateHome();
|
|
79
|
+
});
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
82
|
+
if (prevHome === undefined)
|
|
83
|
+
delete process.env.HOME;
|
|
84
|
+
else
|
|
85
|
+
process.env.HOME = prevHome;
|
|
86
|
+
if (prevAwm === undefined)
|
|
87
|
+
delete process.env.AWM_HOME;
|
|
88
|
+
else
|
|
89
|
+
process.env.AWM_HOME = prevAwm;
|
|
90
|
+
});
|
|
91
|
+
it('exports CONTEXT_INJECT_MAX_CHARS = 2048', () => {
|
|
92
|
+
expect(resolve_1.CONTEXT_INJECT_MAX_CHARS).toBe(2048);
|
|
93
|
+
});
|
|
94
|
+
it('durable value stays fresh after fake clock +1y (RF-2.3)', () => {
|
|
95
|
+
const store = {
|
|
96
|
+
'repos.root': {
|
|
97
|
+
value: '/x',
|
|
98
|
+
status: 'fresh',
|
|
99
|
+
updatedAt: '2025-01-01T00:00:00.000Z',
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
const now = new Date('2026-01-01T00:00:00.000Z');
|
|
103
|
+
expect((0, resolve_1.statusFor)(durableCatalog, store, now)).toEqual({ 'repos.root': 'fresh' });
|
|
104
|
+
const injection = (0, resolve_1.buildInjection)({
|
|
105
|
+
catalog: durableCatalog,
|
|
106
|
+
store,
|
|
107
|
+
storePath: '/store/values.json',
|
|
108
|
+
inject: 'reference',
|
|
109
|
+
now,
|
|
110
|
+
});
|
|
111
|
+
expect(injection.mode).toBe('reference');
|
|
112
|
+
expect(injection.values).toBeUndefined();
|
|
113
|
+
});
|
|
114
|
+
it('volatile+ttl elapsed + inject:context does not serve secret as fresh (RF-2.4/2.5)', () => {
|
|
115
|
+
const secret = 'secret-should-not-serve-as-fresh';
|
|
116
|
+
const store = {
|
|
117
|
+
'aws.token': {
|
|
118
|
+
value: secret,
|
|
119
|
+
status: 'fresh',
|
|
120
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
121
|
+
expiresAt: '2026-10-12T00:00:01.000Z',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
const now = new Date('2026-10-12T00:00:02.000Z');
|
|
125
|
+
expect((0, resolve_1.statusFor)(volatileContextCatalog, store, now)['aws.token']).toBe('stale');
|
|
126
|
+
// Catalog inject:context + request inject:context so omitting the stale/fresh
|
|
127
|
+
// gate would put `secret` into values and flip mode to context (mutation-proof).
|
|
128
|
+
const injection = (0, resolve_1.buildInjection)({
|
|
129
|
+
catalog: volatileContextCatalog,
|
|
130
|
+
store,
|
|
131
|
+
storePath: '/store/values.json',
|
|
132
|
+
inject: 'context',
|
|
133
|
+
now,
|
|
134
|
+
maxChars: 10_000,
|
|
135
|
+
});
|
|
136
|
+
expect(injection.mode).toBe('reference');
|
|
137
|
+
expect(injection.values).toBeUndefined();
|
|
138
|
+
expect(injection.values?.['aws.token']).toBeUndefined();
|
|
139
|
+
expect(JSON.stringify(injection)).not.toContain(secret);
|
|
140
|
+
});
|
|
141
|
+
it('stored status stale + inject:context does not inline the value (RF-2.4/2.5)', () => {
|
|
142
|
+
const secret = 'stale-context-secret-value';
|
|
143
|
+
const store = {
|
|
144
|
+
'aws.token': {
|
|
145
|
+
value: secret,
|
|
146
|
+
status: 'stale',
|
|
147
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
148
|
+
expiresAt: '2026-10-12T00:00:01.000Z',
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
const now = new Date('2026-10-12T00:00:00.500Z'); // before expiresAt; stored status still stale
|
|
152
|
+
expect((0, resolve_1.statusFor)(volatileContextCatalog, store, now)['aws.token']).toBe('stale');
|
|
153
|
+
const injection = (0, resolve_1.buildInjection)({
|
|
154
|
+
catalog: volatileContextCatalog,
|
|
155
|
+
store,
|
|
156
|
+
storePath: '/store/values.json',
|
|
157
|
+
inject: 'context',
|
|
158
|
+
now,
|
|
159
|
+
maxChars: 10_000,
|
|
160
|
+
});
|
|
161
|
+
expect(injection.mode).toBe('reference');
|
|
162
|
+
expect(injection.values).toBeUndefined();
|
|
163
|
+
expect(JSON.stringify(injection)).not.toContain(secret);
|
|
164
|
+
});
|
|
165
|
+
it('missing fact → status missing, no default (RF-2.5)', () => {
|
|
166
|
+
expect((0, resolve_1.statusFor)(durableCatalog, {}, new Date())['repos.root']).toBe('missing');
|
|
167
|
+
const injection = (0, resolve_1.buildInjection)({
|
|
168
|
+
catalog: durableCatalog,
|
|
169
|
+
store: {},
|
|
170
|
+
storePath: '/store/values.json',
|
|
171
|
+
inject: 'context',
|
|
172
|
+
maxChars: 10_000,
|
|
173
|
+
});
|
|
174
|
+
expect(injection.values).toBeUndefined();
|
|
175
|
+
expect(injection.factIds).toContain('repos.root');
|
|
176
|
+
});
|
|
177
|
+
it('buildInjection({ inject: reference }) returns store path + ids, no values (RF-3.1)', () => {
|
|
178
|
+
const store = {
|
|
179
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
180
|
+
'team.nick': { value: 'ace', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
181
|
+
};
|
|
182
|
+
const result = (0, resolve_1.buildInjection)({
|
|
183
|
+
catalog: contextCatalog,
|
|
184
|
+
store,
|
|
185
|
+
storePath: '/abs/environments/default/values.json',
|
|
186
|
+
inject: 'reference',
|
|
187
|
+
});
|
|
188
|
+
expect(result).toMatchObject({
|
|
189
|
+
mode: 'reference',
|
|
190
|
+
storePath: '/abs/environments/default/values.json',
|
|
191
|
+
});
|
|
192
|
+
expect(result.factIds).toEqual(expect.arrayContaining(['team.color', 'team.nick', 'repos.root']));
|
|
193
|
+
expect(result.values).toBeUndefined();
|
|
194
|
+
expect(JSON.stringify(result)).not.toContain('blue');
|
|
195
|
+
expect(JSON.stringify(result)).not.toContain('ace');
|
|
196
|
+
});
|
|
197
|
+
it('buildInjection omits inject option → default reference; no values even when context-fresh exist (RNF-2)', () => {
|
|
198
|
+
const store = {
|
|
199
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
200
|
+
'team.nick': { value: 'ace', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
201
|
+
};
|
|
202
|
+
// No `inject` field: default must be reference. Mutating default to `context` would
|
|
203
|
+
// include blue/ace and fail these assertions.
|
|
204
|
+
const result = (0, resolve_1.buildInjection)({
|
|
205
|
+
catalog: contextCatalog,
|
|
206
|
+
store,
|
|
207
|
+
storePath: '/abs/environments/default/values.json',
|
|
208
|
+
maxChars: resolve_1.CONTEXT_INJECT_MAX_CHARS,
|
|
209
|
+
});
|
|
210
|
+
expect(result.mode).toBe('reference');
|
|
211
|
+
expect(result.values).toBeUndefined();
|
|
212
|
+
expect(JSON.stringify(result)).not.toContain('blue');
|
|
213
|
+
expect(JSON.stringify(result)).not.toContain('ace');
|
|
214
|
+
});
|
|
215
|
+
it('unknown/typo inject does not fail open into context; no values inlined (RNF-2)', () => {
|
|
216
|
+
const store = {
|
|
217
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
218
|
+
'team.nick': { value: 'ace', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
219
|
+
};
|
|
220
|
+
// Runtime typo must not fall through into the context branch (fail-open).
|
|
221
|
+
const result = (0, resolve_1.buildInjection)({
|
|
222
|
+
catalog: contextCatalog,
|
|
223
|
+
store,
|
|
224
|
+
storePath: '/store/values.json',
|
|
225
|
+
inject: 'contezt',
|
|
226
|
+
maxChars: resolve_1.CONTEXT_INJECT_MAX_CHARS,
|
|
227
|
+
});
|
|
228
|
+
expect(result.mode).toBe('reference');
|
|
229
|
+
expect(result.values).toBeUndefined();
|
|
230
|
+
expect(JSON.stringify(result)).not.toContain('blue');
|
|
231
|
+
expect(JSON.stringify(result)).not.toContain('ace');
|
|
232
|
+
});
|
|
233
|
+
it('inject: context under budget includes fresh context values (RF-3.2)', () => {
|
|
234
|
+
const store = {
|
|
235
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
236
|
+
'team.nick': { value: 'ace', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
237
|
+
'repos.root': { value: '/should-not-inline', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
238
|
+
};
|
|
239
|
+
const result = (0, resolve_1.buildInjection)({
|
|
240
|
+
catalog: contextCatalog,
|
|
241
|
+
store,
|
|
242
|
+
storePath: '/store/values.json',
|
|
243
|
+
inject: 'context',
|
|
244
|
+
maxChars: resolve_1.CONTEXT_INJECT_MAX_CHARS,
|
|
245
|
+
});
|
|
246
|
+
expect(result.mode).toBe('context');
|
|
247
|
+
expect(result.values).toEqual({
|
|
248
|
+
'team.color': 'blue',
|
|
249
|
+
'team.nick': 'ace',
|
|
250
|
+
});
|
|
251
|
+
expect(result.values).not.toHaveProperty('repos.root');
|
|
252
|
+
expect(result.warning).toBeUndefined();
|
|
253
|
+
});
|
|
254
|
+
it('inject: context over budget degrades to reference + warning (RF-3.2)', () => {
|
|
255
|
+
const store = {
|
|
256
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
257
|
+
'team.nick': { value: 'ace', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
258
|
+
};
|
|
259
|
+
const result = (0, resolve_1.buildInjection)({
|
|
260
|
+
catalog: contextCatalog,
|
|
261
|
+
store,
|
|
262
|
+
storePath: '/store/values.json',
|
|
263
|
+
inject: 'context',
|
|
264
|
+
maxChars: 1,
|
|
265
|
+
});
|
|
266
|
+
expect(result.mode).toBe('reference');
|
|
267
|
+
expect(result.values).toBeUndefined();
|
|
268
|
+
expect(result.warning).toMatch(/budget|degrad/i);
|
|
269
|
+
expect(result.storePath).toBe('/store/values.json');
|
|
270
|
+
expect(JSON.stringify(result)).not.toMatch(/blue|ace/);
|
|
271
|
+
});
|
|
272
|
+
it('writeFact + statusFor: volatile ttl:1 with past updatedAt is stale', () => {
|
|
273
|
+
(0, store_1.writeFact)('default', 'aws.token', {
|
|
274
|
+
value: 'tok',
|
|
275
|
+
status: 'fresh',
|
|
276
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
277
|
+
}, {
|
|
278
|
+
definition: volatileCatalog.facts['aws.token'],
|
|
279
|
+
});
|
|
280
|
+
const store = (0, store_1.readValues)('default');
|
|
281
|
+
const now = new Date('2026-10-12T00:00:02.000Z');
|
|
282
|
+
expect((0, resolve_1.statusFor)(volatileCatalog, store, now)['aws.token']).toBe('stale');
|
|
283
|
+
});
|
|
284
|
+
it('volatile: now >= expiresAt is stale; missing/garbage expiresAt is stale; null now throws', () => {
|
|
285
|
+
const expired = {
|
|
286
|
+
'aws.token': {
|
|
287
|
+
value: 'tok',
|
|
288
|
+
status: 'fresh',
|
|
289
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
290
|
+
expiresAt: '2026-10-12T00:00:01.000Z',
|
|
291
|
+
},
|
|
292
|
+
};
|
|
293
|
+
// Exact boundary: now === expiresAt → stale (>=, not >).
|
|
294
|
+
expect((0, resolve_1.statusFor)(volatileCatalog, expired, new Date('2026-10-12T00:00:01.000Z'))['aws.token']).toBe('stale');
|
|
295
|
+
// One ms before boundary stays fresh.
|
|
296
|
+
expect((0, resolve_1.statusFor)(volatileCatalog, expired, new Date('2026-10-12T00:00:00.999Z'))['aws.token']).toBe('fresh');
|
|
297
|
+
const missingExpires = {
|
|
298
|
+
'aws.token': {
|
|
299
|
+
value: 'tok',
|
|
300
|
+
status: 'fresh',
|
|
301
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
expect((0, resolve_1.statusFor)(volatileCatalog, missingExpires, new Date())['aws.token']).toBe('stale');
|
|
305
|
+
const garbageExpires = {
|
|
306
|
+
'aws.token': {
|
|
307
|
+
value: 'tok',
|
|
308
|
+
status: 'fresh',
|
|
309
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
310
|
+
expiresAt: 'not-a-date',
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
expect((0, resolve_1.statusFor)(volatileCatalog, garbageExpires, new Date())['aws.token']).toBe('stale');
|
|
314
|
+
expect(() => (0, resolve_1.statusFor)(volatileCatalog, expired, null)).toThrow(/now|Date/i);
|
|
315
|
+
expect(() => (0, resolve_1.buildInjection)({
|
|
316
|
+
catalog: volatileCatalog,
|
|
317
|
+
store: expired,
|
|
318
|
+
storePath: '/store/values.json',
|
|
319
|
+
now: new Date(Number.NaN),
|
|
320
|
+
})).toThrow(/now|Date/i);
|
|
321
|
+
});
|
|
322
|
+
it('inject: context mixed fresh+stale inlines only fresh (RF-3.2)', () => {
|
|
323
|
+
const store = {
|
|
324
|
+
'team.color': { value: 'blue', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
325
|
+
'team.nick': { value: 'stale-nick', status: 'stale', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
326
|
+
};
|
|
327
|
+
const result = (0, resolve_1.buildInjection)({
|
|
328
|
+
catalog: contextCatalog,
|
|
329
|
+
store,
|
|
330
|
+
storePath: '/store/values.json',
|
|
331
|
+
inject: 'context',
|
|
332
|
+
maxChars: resolve_1.CONTEXT_INJECT_MAX_CHARS,
|
|
333
|
+
});
|
|
334
|
+
expect(result.mode).toBe('context');
|
|
335
|
+
expect(result.values).toEqual({ 'team.color': 'blue' });
|
|
336
|
+
expect(result.values).not.toHaveProperty('team.nick');
|
|
337
|
+
expect(JSON.stringify(result)).not.toContain('stale-nick');
|
|
338
|
+
});
|
|
339
|
+
it('buildInjection rejects non-array factIds (string must not spread to chars)', () => {
|
|
340
|
+
expect(() => (0, resolve_1.buildInjection)({
|
|
341
|
+
catalog: durableCatalog,
|
|
342
|
+
store: {},
|
|
343
|
+
storePath: '/store/values.json',
|
|
344
|
+
factIds: 'repos.root',
|
|
345
|
+
})).toThrow(/factIds/i);
|
|
346
|
+
});
|
|
347
|
+
});
|
|
@@ -0,0 +1,248 @@
|
|
|
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 store_1 = require("../../../src/core/environments/store");
|
|
10
|
+
const registries_1 = require("../../../src/core/registries");
|
|
11
|
+
function isolateHome() {
|
|
12
|
+
const tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-env-store-'));
|
|
13
|
+
process.env.AWM_HOME = path_1.default.join(tmp, '.awm');
|
|
14
|
+
process.env.HOME = tmp;
|
|
15
|
+
return tmp;
|
|
16
|
+
}
|
|
17
|
+
describe('environment store (RF-2.1, RF-2.2, RNF-1)', () => {
|
|
18
|
+
let tmp;
|
|
19
|
+
let prevHome;
|
|
20
|
+
let prevAwm;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
prevHome = process.env.HOME;
|
|
23
|
+
prevAwm = process.env.AWM_HOME;
|
|
24
|
+
tmp = isolateHome();
|
|
25
|
+
});
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
fs_1.default.rmSync(tmp, { recursive: true, force: true });
|
|
28
|
+
if (prevHome === undefined)
|
|
29
|
+
delete process.env.HOME;
|
|
30
|
+
else
|
|
31
|
+
process.env.HOME = prevHome;
|
|
32
|
+
if (prevAwm === undefined)
|
|
33
|
+
delete process.env.AWM_HOME;
|
|
34
|
+
else
|
|
35
|
+
process.env.AWM_HOME = prevAwm;
|
|
36
|
+
});
|
|
37
|
+
it('environmentsRoot is under AWM_HOME/environments (RF-2.1)', () => {
|
|
38
|
+
expect((0, store_1.environmentsRoot)()).toBe(path_1.default.join(process.env.AWM_HOME, 'environments'));
|
|
39
|
+
});
|
|
40
|
+
it('set writes only under AWM_HOME/environments/default/values.json (RF-2.1)', () => {
|
|
41
|
+
(0, store_1.writeFact)('default', 'repos.root', {
|
|
42
|
+
value: '/machine/repos',
|
|
43
|
+
status: 'fresh',
|
|
44
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
45
|
+
});
|
|
46
|
+
const expected = path_1.default.join(process.env.AWM_HOME, 'environments', 'default', 'values.json');
|
|
47
|
+
expect((0, store_1.valuesPath)('default')).toBe(expected);
|
|
48
|
+
expect(fs_1.default.existsSync(expected)).toBe(true);
|
|
49
|
+
const parsed = JSON.parse(fs_1.default.readFileSync(expected, 'utf-8'));
|
|
50
|
+
expect(parsed['repos.root']).toMatchObject({
|
|
51
|
+
value: '/machine/repos',
|
|
52
|
+
status: 'fresh',
|
|
53
|
+
});
|
|
54
|
+
// Nothing written outside environments/
|
|
55
|
+
const awm = process.env.AWM_HOME;
|
|
56
|
+
const entries = fs_1.default.readdirSync(awm);
|
|
57
|
+
expect(entries).toEqual(['environments']);
|
|
58
|
+
});
|
|
59
|
+
it('maintains exactly one active environment; first writeFact activates that name (RF-2.2)', () => {
|
|
60
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('default');
|
|
61
|
+
// First materialization activates the env being written — not a hardcoded default.
|
|
62
|
+
(0, store_1.writeFact)('staging', 'repos.root', {
|
|
63
|
+
value: '/a',
|
|
64
|
+
status: 'fresh',
|
|
65
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
66
|
+
});
|
|
67
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('staging');
|
|
68
|
+
(0, store_1.setActiveEnvironmentName)('default');
|
|
69
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('default');
|
|
70
|
+
(0, store_1.writeFact)('default', 'repos.root', {
|
|
71
|
+
value: '/b',
|
|
72
|
+
status: 'fresh',
|
|
73
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
74
|
+
});
|
|
75
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('default');
|
|
76
|
+
(0, store_1.setActiveEnvironmentName)('staging');
|
|
77
|
+
expect((0, store_1.getActiveEnvironmentName)()).toBe('staging');
|
|
78
|
+
});
|
|
79
|
+
it('readValues returns empty object when store file is missing', () => {
|
|
80
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
81
|
+
});
|
|
82
|
+
it('on volatile write sets expiresAt from updatedAt + ttlSeconds', () => {
|
|
83
|
+
const def = {
|
|
84
|
+
valueType: 'string',
|
|
85
|
+
resolution: 'prompt',
|
|
86
|
+
durability: 'volatile',
|
|
87
|
+
inject: 'reference',
|
|
88
|
+
ttl: 60,
|
|
89
|
+
};
|
|
90
|
+
(0, store_1.writeFact)('default', 'aws.token', {
|
|
91
|
+
value: 'tok',
|
|
92
|
+
status: 'fresh',
|
|
93
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
94
|
+
}, { definition: def });
|
|
95
|
+
const stored = (0, store_1.readValues)('default')['aws.token'];
|
|
96
|
+
expect(stored.expiresAt).toBe('2026-10-12T00:01:00.000Z');
|
|
97
|
+
});
|
|
98
|
+
it('writeEnvValue refuses a target inside a registry tree (RNF-1)', () => {
|
|
99
|
+
const registryRoot = path_1.default.join(tmp, 'fake-registry');
|
|
100
|
+
fs_1.default.mkdirSync(path_1.default.join(registryRoot, 'skills'), { recursive: true });
|
|
101
|
+
const badTarget = path_1.default.join(registryRoot, 'environments', 'default', 'values.json');
|
|
102
|
+
expect(() => (0, store_1.writeEnvValue)(badTarget, { 'repos.root': { value: 'x', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' } }, {
|
|
103
|
+
registryRoots: [registryRoot],
|
|
104
|
+
})).toThrow(store_1.EnvironmentStoreError);
|
|
105
|
+
expect(() => (0, store_1.writeEnvValue)(badTarget, { 'repos.root': { value: 'x', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' } }, {
|
|
106
|
+
registryRoots: [registryRoot],
|
|
107
|
+
})).toThrow(/registry/i);
|
|
108
|
+
expect(fs_1.default.existsSync(badTarget)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
it('refuses write when AWM_HOME/environments is a symlink into a registry root (RNF-1)', () => {
|
|
111
|
+
const registryRoot = path_1.default.join(tmp, 'fake-registry');
|
|
112
|
+
const registryEnvLanding = path_1.default.join(registryRoot, 'hijacked-env');
|
|
113
|
+
fs_1.default.mkdirSync(registryEnvLanding, { recursive: true });
|
|
114
|
+
const awm = process.env.AWM_HOME;
|
|
115
|
+
fs_1.default.mkdirSync(awm, { recursive: true });
|
|
116
|
+
// Lexical path is under AWM_HOME; realpath lands inside the registry.
|
|
117
|
+
fs_1.default.symlinkSync(registryEnvLanding, path_1.default.join(awm, 'environments'));
|
|
118
|
+
expect(() => (0, store_1.writeFact)('default', 'repos.root', { value: '/leaked', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, { registryRoots: [registryRoot] })).toThrow(store_1.EnvironmentStoreError);
|
|
119
|
+
expect(() => (0, store_1.writeFact)('default', 'repos.root', { value: '/leaked', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, { registryRoots: [registryRoot] })).toThrow(/registry/i);
|
|
120
|
+
// Nothing materialised inside the registry landing dir.
|
|
121
|
+
expect(fs_1.default.readdirSync(registryEnvLanding)).toEqual([]);
|
|
122
|
+
expect(fs_1.default.existsSync(path_1.default.join(registryEnvLanding, 'default', 'values.json'))).toBe(false);
|
|
123
|
+
});
|
|
124
|
+
it('writeFact rejects empty path valueType without mutating store', () => {
|
|
125
|
+
const def = {
|
|
126
|
+
valueType: 'path',
|
|
127
|
+
resolution: 'prompt',
|
|
128
|
+
durability: 'durable',
|
|
129
|
+
inject: 'reference',
|
|
130
|
+
};
|
|
131
|
+
expect(() => (0, store_1.writeFact)('default', 'repos.root', { value: '', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, { definition: def })).toThrow(store_1.EnvironmentStoreError);
|
|
132
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
133
|
+
});
|
|
134
|
+
it('writeFact rejects unknown id when catalog is provided', () => {
|
|
135
|
+
expect(() => (0, store_1.writeFact)('default', 'unknown.fact', { value: 'x', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, {
|
|
136
|
+
catalog: { schemaVersion: 1, facts: { 'repos.root': {
|
|
137
|
+
valueType: 'path',
|
|
138
|
+
resolution: 'prompt',
|
|
139
|
+
durability: 'durable',
|
|
140
|
+
inject: 'reference',
|
|
141
|
+
} } },
|
|
142
|
+
consultedRoots: ['/tmp/reg-a', '/tmp/reg-b'],
|
|
143
|
+
})).toThrow(/unknown\.fact/);
|
|
144
|
+
expect(() => (0, store_1.writeFact)('default', 'unknown.fact', { value: 'x', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, {
|
|
145
|
+
catalog: { schemaVersion: 1, facts: {} },
|
|
146
|
+
consultedRoots: ['/tmp/reg-a'],
|
|
147
|
+
})).toThrow(/reg-a/);
|
|
148
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
149
|
+
});
|
|
150
|
+
it('writeFact with catalog rejects prototype key id toString', () => {
|
|
151
|
+
expect(() => (0, store_1.writeFact)('default', 'toString', { value: 'x', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, {
|
|
152
|
+
catalog: {
|
|
153
|
+
schemaVersion: 1,
|
|
154
|
+
facts: {
|
|
155
|
+
'repos.root': {
|
|
156
|
+
valueType: 'path',
|
|
157
|
+
resolution: 'prompt',
|
|
158
|
+
durability: 'durable',
|
|
159
|
+
inject: 'reference',
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
consultedRoots: ['/tmp/reg'],
|
|
164
|
+
})).toThrow(/toString|Unknown fact/i);
|
|
165
|
+
expect((0, store_1.readValues)('default')).toEqual({});
|
|
166
|
+
});
|
|
167
|
+
it('assertEnvName rejects NUL, meta.json, values.json (case-insensitive)', () => {
|
|
168
|
+
expect(() => (0, store_1.writeFact)('meta.json', 'repos.root', {
|
|
169
|
+
value: '/x',
|
|
170
|
+
status: 'fresh',
|
|
171
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
172
|
+
})).toThrow(store_1.EnvironmentStoreError);
|
|
173
|
+
expect(() => (0, store_1.writeFact)('META.JSON', 'repos.root', {
|
|
174
|
+
value: '/x',
|
|
175
|
+
status: 'fresh',
|
|
176
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
177
|
+
})).toThrow(/reserved/i);
|
|
178
|
+
expect(() => (0, store_1.writeFact)('Values.json', 'repos.root', {
|
|
179
|
+
value: '/x',
|
|
180
|
+
status: 'fresh',
|
|
181
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
182
|
+
})).toThrow(/reserved/i);
|
|
183
|
+
expect(() => (0, store_1.writeFact)('values.json', 'repos.root', {
|
|
184
|
+
value: '/x',
|
|
185
|
+
status: 'fresh',
|
|
186
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
187
|
+
})).toThrow(store_1.EnvironmentStoreError);
|
|
188
|
+
expect(() => (0, store_1.writeFact)('bad\0name', 'repos.root', {
|
|
189
|
+
value: '/x',
|
|
190
|
+
status: 'fresh',
|
|
191
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
192
|
+
})).toThrow(store_1.EnvironmentStoreError);
|
|
193
|
+
expect(() => (0, store_1.setActiveEnvironmentName)(' ')).toThrow(store_1.EnvironmentStoreError);
|
|
194
|
+
});
|
|
195
|
+
it('readValues rejects __proto__ fact id (prototype pollution)', () => {
|
|
196
|
+
const file = (0, store_1.valuesPath)('default');
|
|
197
|
+
fs_1.default.mkdirSync(path_1.default.dirname(file), { recursive: true });
|
|
198
|
+
// Raw JSON — object-literal __proto__ would set [[Prototype]], not an own key.
|
|
199
|
+
fs_1.default.writeFileSync(file, '{"__proto__":{"value":"x","status":"fresh","updatedAt":"2026-10-12T00:00:00.000Z"}}\n');
|
|
200
|
+
expect(() => (0, store_1.readValues)('default')).toThrow(/forbidden|__proto__/i);
|
|
201
|
+
});
|
|
202
|
+
it('getActiveEnvironmentName asserts active from meta (corrupt → throw)', () => {
|
|
203
|
+
const meta = path_1.default.join(process.env.AWM_HOME, 'environments', 'meta.json');
|
|
204
|
+
fs_1.default.mkdirSync(path_1.default.dirname(meta), { recursive: true });
|
|
205
|
+
fs_1.default.writeFileSync(meta, JSON.stringify({ active: 'meta.json' }) + '\n');
|
|
206
|
+
expect(() => (0, store_1.getActiveEnvironmentName)()).toThrow(store_1.EnvironmentStoreError);
|
|
207
|
+
});
|
|
208
|
+
it('registryRoots:[] still unions contentRoots() — cannot bypass RNF-1', () => {
|
|
209
|
+
const regName = 'guard-reg';
|
|
210
|
+
(0, registries_1.writeRegistriesConfig)([{ name: regName, remote: 'https://example.test/guard.git' }]);
|
|
211
|
+
const regRoot = (0, registries_1.registryContentRoot)(regName);
|
|
212
|
+
fs_1.default.mkdirSync(path_1.default.join(regRoot, 'skills'), { recursive: true });
|
|
213
|
+
const badTarget = path_1.default.join(regRoot, 'environments', 'default', 'values.json');
|
|
214
|
+
// Explicit empty array must NOT disable checks — contentRoots() still applies.
|
|
215
|
+
expect(() => (0, store_1.writeEnvValue)(badTarget, {
|
|
216
|
+
'repos.root': {
|
|
217
|
+
value: '/leaked',
|
|
218
|
+
status: 'fresh',
|
|
219
|
+
updatedAt: '2026-10-12T00:00:00.000Z',
|
|
220
|
+
},
|
|
221
|
+
}, { registryRoots: [] })).toThrow(/registry/i);
|
|
222
|
+
expect(fs_1.default.existsSync(badTarget)).toBe(false);
|
|
223
|
+
});
|
|
224
|
+
it('writeEnvValue validates each stored entry before write', () => {
|
|
225
|
+
const target = path_1.default.join(process.env.AWM_HOME, 'environments', 'default', 'values.json');
|
|
226
|
+
expect(() => (0, store_1.writeEnvValue)(target, {
|
|
227
|
+
'repos.root': { value: 1, status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' },
|
|
228
|
+
})).toThrow(store_1.EnvironmentStoreError);
|
|
229
|
+
expect(fs_1.default.existsSync(target)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
it('withExpiresAt requires finite ttl for volatile and catches RangeError', () => {
|
|
232
|
+
const noTtl = {
|
|
233
|
+
valueType: 'string',
|
|
234
|
+
resolution: 'prompt',
|
|
235
|
+
durability: 'volatile',
|
|
236
|
+
inject: 'reference',
|
|
237
|
+
};
|
|
238
|
+
expect(() => (0, store_1.writeFact)('default', 'aws.token', { value: 'tok', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, { definition: noTtl })).toThrow(/ttl/i);
|
|
239
|
+
const huge = {
|
|
240
|
+
valueType: 'string',
|
|
241
|
+
resolution: 'prompt',
|
|
242
|
+
durability: 'volatile',
|
|
243
|
+
inject: 'reference',
|
|
244
|
+
ttl: Number.MAX_SAFE_INTEGER,
|
|
245
|
+
};
|
|
246
|
+
expect(() => (0, store_1.writeFact)('default', 'aws.token', { value: 'tok', status: 'fresh', updatedAt: '2026-10-12T00:00:00.000Z' }, { definition: huge })).toThrow(store_1.EnvironmentStoreError);
|
|
247
|
+
});
|
|
248
|
+
});
|