agentic-workflow-manager 9.21.0 → 9.22.1

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.
Files changed (71) hide show
  1. package/dist/src/commands/change/index.js +16 -5
  2. package/dist/src/commands/init.js +8 -4
  3. package/dist/src/commands/preflight/checks.js +22 -0
  4. package/dist/src/commands/registry/add.js +9 -3
  5. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  6. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  7. package/dist/src/commands/sensors/index.js +10 -1
  8. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  9. package/dist/src/commands/sensors/prepare.js +10 -0
  10. package/dist/src/commands/sensors/result-cache.js +278 -0
  11. package/dist/src/commands/sensors/result.js +31 -12
  12. package/dist/src/commands/sensors/run.js +265 -8
  13. package/dist/src/commands/sensors/status.js +66 -7
  14. package/dist/src/commands/sync.js +11 -5
  15. package/dist/src/commands/update.js +23 -12
  16. package/dist/src/core/changes/derive.js +6 -3
  17. package/dist/src/core/changes/git.js +1 -2
  18. package/dist/src/core/changes/receipt.js +219 -9
  19. package/dist/src/core/changes/worktree.js +15 -3
  20. package/dist/src/core/init/steps.js +3 -8
  21. package/dist/src/core/registries.js +20 -15
  22. package/dist/src/core/registry-git-errors.js +107 -0
  23. package/dist/src/core/registry-git.js +93 -0
  24. package/dist/src/core/registry-sync-report.js +74 -0
  25. package/dist/src/core/versioning.js +3 -6
  26. package/dist/src/index.js +5 -8
  27. package/dist/tests/commands/change/new.test.js +104 -58
  28. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  29. package/dist/tests/commands/change/validate.test.js +164 -108
  30. package/dist/tests/commands/changes/list.test.js +41 -22
  31. package/dist/tests/commands/job/gate-reconcile.test.js +13 -2
  32. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  33. package/dist/tests/commands/registry-sync-honesty.test.js +175 -0
  34. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  35. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  36. package/dist/tests/commands/sensors/exec.test.js +3 -1
  37. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  38. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  39. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  40. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  41. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  42. package/dist/tests/commands/update.test.js +8 -8
  43. package/dist/tests/commands/watch/supervisor-loop.test.js +21 -5
  44. package/dist/tests/core/changes/helpers.js +15 -3
  45. package/dist/tests/core/changes/list-paging.test.js +131 -110
  46. package/dist/tests/core/changes/list.test.js +133 -83
  47. package/dist/tests/core/changes/receipt.test.js +279 -8
  48. package/dist/tests/core/diagnostics/provider-checks.test.js +3 -0
  49. package/dist/tests/core/init/steps-registry-sync.test.js +6 -8
  50. package/dist/tests/core/registry-git.test.js +153 -0
  51. package/dist/tests/helpers/commit-seeded-registry.js +18 -0
  52. package/dist/tests/integration/codex-provider-isolated.test.js +7 -2
  53. package/dist/tests/integration/copilot-init-isolated.test.js +7 -4
  54. package/dist/tests/structural/active-documentation.test.js +0 -41
  55. package/dist/tests/structural/bundle-skill-reference-contract.test.js +0 -12
  56. package/dist/tests/structural/cli-reference-matches-commander.test.js +94 -0
  57. package/dist/tests/structural/native-release-artifacts.test.js +116 -107
  58. package/dist/tests/structural/sensor-documentation-contract.test.js +0 -47
  59. package/dist/tests/structural/sensor-portability-contract.test.js +0 -7
  60. package/dist/tests/structural/simple-git-v4-pin.test.js +49 -0
  61. package/package.json +3 -2
  62. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  63. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  64. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  65. package/prebuilds/win32-x64/secure_fs.node +0 -0
  66. package/dist/tests/structural/cloud-admission-docs-contract.test.js +0 -54
  67. package/dist/tests/structural/documentation-phase-is-mechanized.test.js +0 -29
  68. package/dist/tests/structural/operator-environments-docs-contract.test.js +0 -70
  69. package/dist/tests/structural/process-model-description-contract.test.js +0 -34
  70. package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +0 -104
  71. package/dist/tests/structural/r4-plan-currentness-docs.test.js +0 -37
@@ -0,0 +1,373 @@
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 child_process_1 = require("child_process");
10
+ const posix_1 = __importDefault(require("picomatch/posix"));
11
+ const inputs_digest_1 = require("../../../src/commands/sensors/inputs-digest");
12
+ function git(cwd, args) {
13
+ return (0, child_process_1.execFileSync)('git', ['-c', 'user.email=t@t.t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], {
14
+ cwd,
15
+ encoding: 'utf8',
16
+ stdio: ['ignore', 'pipe', 'pipe'],
17
+ });
18
+ }
19
+ function initRepo() {
20
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-'));
21
+ git(root, ['init', '-q', '-b', 'main']);
22
+ return root;
23
+ }
24
+ describe('inputs digest (RF-2.1, RF-2.12, RF-1.6)', () => {
25
+ const roots = [];
26
+ afterEach(() => {
27
+ for (const root of roots.splice(0))
28
+ fs_1.default.rmSync(root, { recursive: true, force: true });
29
+ });
30
+ it('toSafeRepoRel realpaths both sides so nested .awm/sensors.json stays relative (Windows-safe)', () => {
31
+ const root = initRepo();
32
+ roots.push(root);
33
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
34
+ const manifest = path_1.default.join(root, '.awm', 'sensors.json');
35
+ fs_1.default.writeFileSync(manifest, '{}');
36
+ // Non-canonical roots (trailing `.`, resolve vs join) must still yield a POSIX repo-rel —
37
+ // this is the class of git-toplevel vs Node path mismatch that broke win32 CI.
38
+ expect((0, inputs_digest_1.toSafeRepoRel)(root, manifest, 'sensorsJsonRel')).toBe('.awm/sensors.json');
39
+ expect((0, inputs_digest_1.toSafeRepoRel)(path_1.default.join(root, '.'), path_1.default.resolve(root, '.awm', 'sensors.json'), 'sensorsJsonRel')).toBe('.awm/sensors.json');
40
+ expect((0, inputs_digest_1.toSafeRepoRel)(root, root, 'packageRoot')).toBe('');
41
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-out-'));
42
+ roots.push(outside);
43
+ expect(() => (0, inputs_digest_1.toSafeRepoRel)(root, outside, 'escape')).toThrow(/under gitRoot|safe relative/);
44
+ });
45
+ it('joinRepoRel builds baseline validate path without touching the filesystem (Windows-safe)', () => {
46
+ // Missing .awm/sensors.baseline.json must not go through toSafeRepoRel(abs) — on win32
47
+ // realpath fallback + path.relative yields a drive-absolute string.
48
+ expect(inputs_digest_1.CACHE_DIGEST_BASELINE_REL).toBe('.awm/sensors.baseline.json');
49
+ expect((0, inputs_digest_1.joinRepoRel)('', inputs_digest_1.CACHE_DIGEST_BASELINE_REL)).toBe('.awm/sensors.baseline.json');
50
+ expect((0, inputs_digest_1.joinRepoRel)('cli', inputs_digest_1.CACHE_DIGEST_BASELINE_REL)).toBe('cli/.awm/sensors.baseline.json');
51
+ });
52
+ it('computeCacheInputDigest accepts non-canonical gitRoot/packageRoot (Windows-safe)', () => {
53
+ const root = initRepo();
54
+ roots.push(root);
55
+ fs_1.default.writeFileSync(path_1.default.join(root, 'src.ts'), 'export {}\n');
56
+ git(root, ['add', 'src.ts']);
57
+ git(root, ['commit', '-q', '-m', 'init']);
58
+ // Same physical tree via join(root, '.') — mirrors mismatched forms that made
59
+ // bare path.relative + path.isAbsolute reject packageRoot on win32.
60
+ const messyRoot = path_1.default.join(root, '.');
61
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
62
+ gitRoot: messyRoot,
63
+ packageRoot: path_1.default.resolve(root),
64
+ inputGlobs: ['*.ts'],
65
+ alwaysMeasuredRepoRels: [],
66
+ });
67
+ expect(digest.ok).toBe(true);
68
+ if (!digest.ok)
69
+ return;
70
+ expect(digest.matchedPaths).toContain('src.ts');
71
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-pkg-out-'));
72
+ roots.push(outside);
73
+ expect(() => (0, inputs_digest_1.computeCacheInputDigest)({
74
+ gitRoot: root,
75
+ packageRoot: outside,
76
+ inputGlobs: ['*.ts'],
77
+ alwaysMeasuredRepoRels: [],
78
+ })).toThrow(/packageRoot must be inside gitRoot/);
79
+ });
80
+ it('uses picomatch options that match dotfiles and stay case-sensitive (RF-1.5/1.6)', () => {
81
+ const withOpts = (0, posix_1.default)('*', { ...inputs_digest_1.PICOMATCH_DIGEST_OPTIONS });
82
+ expect(withOpts('.env')).toBe(true);
83
+ expect(withOpts('Env')).toBe(true);
84
+ // Without dot:true, leading-dot files would not match `*`.
85
+ expect((0, posix_1.default)('*', { posix: true, dot: false })('.env')).toBe(false);
86
+ // Without nocase, Abc and abc are distinct under a case-sensitive pattern.
87
+ const caseSensitive = (0, posix_1.default)('Abc', { ...inputs_digest_1.PICOMATCH_DIGEST_OPTIONS });
88
+ expect(caseSensitive('Abc')).toBe(true);
89
+ expect(caseSensitive('abc')).toBe(false);
90
+ expect(inputs_digest_1.PICOMATCH_DIGEST_OPTIONS).toEqual({ dot: true, posix: true });
91
+ });
92
+ it('enumerates via git ls-files, matches globs with dotfiles, hashes symlink readlink, and marks ABSENT (RF-2.1)', () => {
93
+ const root = initRepo();
94
+ roots.push(root);
95
+ fs_1.default.writeFileSync(path_1.default.join(root, 'src.ts'), 'a');
96
+ fs_1.default.writeFileSync(path_1.default.join(root, '.hidden.ts'), 'h');
97
+ fs_1.default.writeFileSync(path_1.default.join(root, 'ignored.ts'), 'i');
98
+ fs_1.default.writeFileSync(path_1.default.join(root, '.gitignore'), 'ignored.ts\n');
99
+ fs_1.default.symlinkSync('src.ts', path_1.default.join(root, 'link.ts'));
100
+ git(root, ['add', '-A']);
101
+ git(root, ['commit', '-qm', 'init']);
102
+ // Deleted but still listed if we force a stale index entry via ls-files after delete without refresh:
103
+ // Prefer: path in globs that is untracked absent — RF says ENOENT on lstat of listed path.
104
+ fs_1.default.writeFileSync(path_1.default.join(root, 'gone.ts'), 'g');
105
+ git(root, ['add', 'gone.ts']);
106
+ git(root, ['commit', '-qm', 'gone']);
107
+ fs_1.default.unlinkSync(path_1.default.join(root, 'gone.ts'));
108
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
109
+ gitRoot: root,
110
+ packageRoot: root,
111
+ inputGlobs: ['**/*.ts'],
112
+ alwaysMeasuredRepoRels: [],
113
+ });
114
+ expect(digest.ok).toBe(true);
115
+ if (!digest.ok)
116
+ return;
117
+ expect(digest.matchedPaths).toEqual(expect.arrayContaining(['src.ts', '.hidden.ts', 'link.ts', 'gone.ts']));
118
+ expect(digest.matchedPaths).not.toContain('ignored.ts');
119
+ expect(digest.contributions.find((c) => c.path === 'link.ts')).toMatchObject({ kind: 'symlink' });
120
+ expect(digest.contributions.find((c) => c.path === 'gone.ts')).toMatchObject({ kind: 'absent', marker: inputs_digest_1.ABSENT_MARKER });
121
+ expect(digest.digest).toMatch(/^sha256:[0-9a-f]{64}$/);
122
+ });
123
+ it('includes always-measured sensors.json, assets, configFiles, package.json, lockfiles — not baseline.json (RF-2.12)', () => {
124
+ const root = initRepo();
125
+ roots.push(root);
126
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
127
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), '{"pack":"js-ts"}');
128
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.baseline.json'), '{"lint":[]}');
129
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{"name":"x"}');
130
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package-lock.json'), '{}');
131
+ fs_1.default.writeFileSync(path_1.default.join(root, 'eslint.config.awm.mjs'), 'export default []');
132
+ fs_1.default.writeFileSync(path_1.default.join(root, 'tsconfig.json'), '{}');
133
+ fs_1.default.writeFileSync(path_1.default.join(root, 'src.ts'), 'x');
134
+ git(root, ['add', '-A']);
135
+ git(root, ['commit', '-qm', 'init']);
136
+ const always = (0, inputs_digest_1.buildAlwaysMeasuredPaths)({
137
+ sensorsJsonRel: '.awm/sensors.json',
138
+ assetRels: ['eslint.config.awm.mjs'],
139
+ configFileRels: ['tsconfig.json'],
140
+ includePackageJson: true,
141
+ packageRootRel: '',
142
+ });
143
+ expect(always).toEqual(expect.arrayContaining([
144
+ '.awm/sensors.json',
145
+ 'eslint.config.awm.mjs',
146
+ 'tsconfig.json',
147
+ 'package.json',
148
+ 'package-lock.json',
149
+ ]));
150
+ expect(always).not.toContain('.awm/sensors.baseline.json');
151
+ const withBaselinePoison = (0, inputs_digest_1.computeCacheInputDigest)({
152
+ gitRoot: root,
153
+ packageRoot: root,
154
+ inputGlobs: ['**/*.ts'],
155
+ alwaysMeasuredRepoRels: always,
156
+ });
157
+ expect(withBaselinePoison.ok).toBe(true);
158
+ if (!withBaselinePoison.ok)
159
+ return;
160
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.baseline.json'), '{"lint":["changed"]}');
161
+ const afterBaseline = (0, inputs_digest_1.computeCacheInputDigest)({
162
+ gitRoot: root,
163
+ packageRoot: root,
164
+ inputGlobs: ['**/*.ts'],
165
+ alwaysMeasuredRepoRels: always,
166
+ });
167
+ expect(afterBaseline.ok).toBe(true);
168
+ if (!afterBaseline.ok)
169
+ return;
170
+ expect(afterBaseline.digest).toBe(withBaselinePoison.digest);
171
+ fs_1.default.writeFileSync(path_1.default.join(root, 'package.json'), '{"name":"y"}');
172
+ const afterPkg = (0, inputs_digest_1.computeCacheInputDigest)({
173
+ gitRoot: root,
174
+ packageRoot: root,
175
+ inputGlobs: ['**/*.ts'],
176
+ alwaysMeasuredRepoRels: always,
177
+ });
178
+ expect(afterPkg.ok).toBe(true);
179
+ if (!afterPkg.ok)
180
+ return;
181
+ expect(afterPkg.digest).not.toBe(withBaselinePoison.digest);
182
+ });
183
+ it('hard-excludes .awm/sensors.baseline.json from cache digest even when globs match it (RF-2.12/2.13)', () => {
184
+ const root = initRepo();
185
+ roots.push(root);
186
+ fs_1.default.mkdirSync(path_1.default.join(root, '.awm'), { recursive: true });
187
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.json'), '{"pack":"js-ts"}');
188
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.baseline.json'), '{"lint":["old"]}');
189
+ fs_1.default.writeFileSync(path_1.default.join(root, 'other.json'), '{"x":1}');
190
+ git(root, ['add', '-A']);
191
+ git(root, ['commit', '-qm', 'init']);
192
+ const before = (0, inputs_digest_1.computeCacheInputDigest)({
193
+ gitRoot: root,
194
+ packageRoot: root,
195
+ inputGlobs: ['**/*.json'],
196
+ alwaysMeasuredRepoRels: ['.awm/sensors.json'],
197
+ });
198
+ expect(before.ok).toBe(true);
199
+ if (!before.ok)
200
+ return;
201
+ expect(before.matchedPaths).not.toContain('.awm/sensors.baseline.json');
202
+ expect(before.matchedPaths).toEqual(expect.arrayContaining(['.awm/sensors.json', 'other.json']));
203
+ fs_1.default.writeFileSync(path_1.default.join(root, '.awm', 'sensors.baseline.json'), '{"lint":["new-fingerprint"]}');
204
+ const afterBaselineEdit = (0, inputs_digest_1.computeCacheInputDigest)({
205
+ gitRoot: root,
206
+ packageRoot: root,
207
+ inputGlobs: ['**/*.json'],
208
+ alwaysMeasuredRepoRels: ['.awm/sensors.json'],
209
+ });
210
+ expect(afterBaselineEdit.ok).toBe(true);
211
+ if (!afterBaselineEdit.ok)
212
+ return;
213
+ expect(afterBaselineEdit.digest).toBe(before.digest);
214
+ fs_1.default.writeFileSync(path_1.default.join(root, 'other.json'), '{"x":2}');
215
+ const afterOther = (0, inputs_digest_1.computeCacheInputDigest)({
216
+ gitRoot: root,
217
+ packageRoot: root,
218
+ inputGlobs: ['**/*.json'],
219
+ alwaysMeasuredRepoRels: ['.awm/sensors.json'],
220
+ });
221
+ expect(afterOther.ok).toBe(true);
222
+ if (!afterOther.ok)
223
+ return;
224
+ expect(afterOther.digest).not.toBe(before.digest);
225
+ });
226
+ it('never follows symlinks when hashing file contents (RF-2.1 security)', () => {
227
+ const root = initRepo();
228
+ roots.push(root);
229
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-out-'));
230
+ roots.push(outside);
231
+ fs_1.default.writeFileSync(path_1.default.join(outside, 'secret'), 'SECRET');
232
+ fs_1.default.symlinkSync(path_1.default.join(outside, 'secret'), path_1.default.join(root, 'trap.ts'));
233
+ git(root, ['add', '-A']);
234
+ git(root, ['commit', '-qm', 'init']);
235
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
236
+ gitRoot: root,
237
+ packageRoot: root,
238
+ inputGlobs: ['**/*.ts'],
239
+ alwaysMeasuredRepoRels: [],
240
+ });
241
+ expect(digest.ok).toBe(true);
242
+ if (!digest.ok)
243
+ return;
244
+ const trap = digest.contributions.find((c) => c.path === 'trap.ts');
245
+ expect(trap?.kind).toBe('symlink');
246
+ expect(JSON.stringify(digest)).not.toContain('SECRET');
247
+ });
248
+ it('fails closed when git cannot list files (RF-E.2)', () => {
249
+ const root = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-nogit-'));
250
+ roots.push(root);
251
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
252
+ gitRoot: root,
253
+ packageRoot: root,
254
+ inputGlobs: ['**/*.ts'],
255
+ alwaysMeasuredRepoRels: [],
256
+ });
257
+ expect(digest).toEqual({ ok: false, reason: 'git-failed' });
258
+ });
259
+ it('matches globs relative to packageRoot in a monorepo layout', () => {
260
+ const root = initRepo();
261
+ roots.push(root);
262
+ fs_1.default.mkdirSync(path_1.default.join(root, 'cli', 'src'), { recursive: true });
263
+ fs_1.default.writeFileSync(path_1.default.join(root, 'cli', 'src', 'a.ts'), 'a');
264
+ fs_1.default.writeFileSync(path_1.default.join(root, 'other.ts'), 'o');
265
+ git(root, ['add', '-A']);
266
+ git(root, ['commit', '-qm', 'init']);
267
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
268
+ gitRoot: root,
269
+ packageRoot: path_1.default.join(root, 'cli'),
270
+ inputGlobs: ['src/**/*.ts'],
271
+ alwaysMeasuredRepoRels: [],
272
+ });
273
+ expect(digest.ok).toBe(true);
274
+ if (!digest.ok)
275
+ return;
276
+ expect(digest.matchedPaths).toContain('cli/src/a.ts');
277
+ expect(digest.matchedPaths).not.toContain('other.ts');
278
+ });
279
+ it('treats intermediate directory symlink escapes as ABSENT and never reads outside secrets (QA-1)', () => {
280
+ const root = initRepo();
281
+ roots.push(root);
282
+ const outside = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-digest-parent-esc-'));
283
+ roots.push(outside);
284
+ fs_1.default.writeFileSync(path_1.default.join(outside, 'secret.ts'), 'OUTSIDE_SECRET_CONTENT');
285
+ fs_1.default.mkdirSync(path_1.default.join(root, 'nested'), { recursive: true });
286
+ fs_1.default.writeFileSync(path_1.default.join(root, 'nested', 'ok.ts'), 'inside');
287
+ git(root, ['add', '-A']);
288
+ git(root, ['commit', '-qm', 'init']);
289
+ // Replace parent dir with a symlink that redirects into OUTSIDE.
290
+ fs_1.default.rmSync(path_1.default.join(root, 'nested'), { recursive: true, force: true });
291
+ fs_1.default.symlinkSync(outside, path_1.default.join(root, 'nested'));
292
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
293
+ gitRoot: root,
294
+ packageRoot: root,
295
+ inputGlobs: ['nested/**/*.ts'],
296
+ alwaysMeasuredRepoRels: ['nested/ok.ts'],
297
+ });
298
+ expect(digest.ok).toBe(true);
299
+ if (!digest.ok)
300
+ return;
301
+ expect(JSON.stringify(digest)).not.toContain('OUTSIDE_SECRET_CONTENT');
302
+ for (const c of digest.contributions.filter((x) => x.path.startsWith('nested/'))) {
303
+ expect(c.kind).toBe('absent');
304
+ expect(c.marker).toBe(inputs_digest_1.ABSENT_MARKER);
305
+ }
306
+ });
307
+ it('rejects unsafe repoRel shapes before join (QA-2)', () => {
308
+ expect((0, inputs_digest_1.isSafeRepoRel)('src/a.ts')).toBe(true);
309
+ expect((0, inputs_digest_1.isSafeRepoRel)('')).toBe(false);
310
+ expect((0, inputs_digest_1.isSafeRepoRel)('.')).toBe(false);
311
+ expect((0, inputs_digest_1.isSafeRepoRel)('..')).toBe(false);
312
+ expect((0, inputs_digest_1.isSafeRepoRel)('../x')).toBe(false);
313
+ expect((0, inputs_digest_1.isSafeRepoRel)('/abs')).toBe(false);
314
+ expect((0, inputs_digest_1.isSafeRepoRel)('a/../b')).toBe(false);
315
+ expect((0, inputs_digest_1.isSafeRepoRel)('a/./b')).toBe(false);
316
+ expect(() => (0, inputs_digest_1.buildAlwaysMeasuredPaths)({
317
+ sensorsJsonRel: '../escape/sensors.json',
318
+ assetRels: [],
319
+ configFileRels: [],
320
+ includePackageJson: false,
321
+ packageRootRel: '',
322
+ })).toThrow(/relative|contain|safe|escape|\.\./i);
323
+ expect(() => (0, inputs_digest_1.buildAlwaysMeasuredPaths)({
324
+ sensorsJsonRel: '.awm/sensors.json',
325
+ assetRels: ['../evil'],
326
+ configFileRels: [],
327
+ includePackageJson: false,
328
+ packageRootRel: 'cli',
329
+ })).toThrow(/relative|contain|safe|escape|\.\./i);
330
+ });
331
+ it('rejects unsafe alwaysMeasuredRepoRels loudly in computeCacheInputDigest', () => {
332
+ const root = initRepo();
333
+ roots.push(root);
334
+ fs_1.default.writeFileSync(path_1.default.join(root, 'a.ts'), 'x');
335
+ git(root, ['add', '-A']);
336
+ git(root, ['commit', '-qm', 'init']);
337
+ expect(() => (0, inputs_digest_1.computeCacheInputDigest)({
338
+ gitRoot: root,
339
+ packageRoot: root,
340
+ inputGlobs: ['**/*.ts'],
341
+ alwaysMeasuredRepoRels: ['../escape'],
342
+ })).toThrow(/alwaysMeasured|safe|relative|escape|\.\./i);
343
+ expect(() => (0, inputs_digest_1.computeCacheInputDigest)({
344
+ gitRoot: root,
345
+ packageRoot: root,
346
+ inputGlobs: ['**/*.ts'],
347
+ alwaysMeasuredRepoRels: ['a/../b.ts'],
348
+ })).toThrow(/alwaysMeasured|safe|relative|escape|\.\./i);
349
+ });
350
+ it('caps hashed file size and fail-closes oversized inputs (QA-6)', () => {
351
+ const root = initRepo();
352
+ roots.push(root);
353
+ const big = path_1.default.join(root, 'big.ts');
354
+ const fd = fs_1.default.openSync(big, 'w');
355
+ fs_1.default.writeSync(fd, Buffer.alloc(1, 0x61));
356
+ fs_1.default.ftruncateSync(fd, inputs_digest_1.MAX_DIGEST_FILE_BYTES + 1);
357
+ fs_1.default.closeSync(fd);
358
+ git(root, ['add', '-A']);
359
+ git(root, ['commit', '-qm', 'big']);
360
+ const digest = (0, inputs_digest_1.computeCacheInputDigest)({
361
+ gitRoot: root,
362
+ packageRoot: root,
363
+ inputGlobs: ['**/*.ts'],
364
+ alwaysMeasuredRepoRels: [],
365
+ });
366
+ expect(digest.ok).toBe(true);
367
+ if (!digest.ok)
368
+ return;
369
+ const contrib = digest.contributions.find((c) => c.path === 'big.ts');
370
+ expect(contrib?.kind).toBe('absent');
371
+ expect(contrib?.marker).toBe(inputs_digest_1.ABSENT_MARKER);
372
+ });
373
+ });