agentic-workflow-manager 9.20.4 → 9.22.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.
Files changed (42) hide show
  1. package/dist/src/commands/change/index.js +14 -3
  2. package/dist/src/commands/preflight/checks.js +31 -0
  3. package/dist/src/commands/sensors/compatibility/contract.js +56 -3
  4. package/dist/src/commands/sensors/compatibility/manifest.js +16 -2
  5. package/dist/src/commands/sensors/index.js +10 -1
  6. package/dist/src/commands/sensors/init.js +7 -1
  7. package/dist/src/commands/sensors/inputs-digest.js +315 -0
  8. package/dist/src/commands/sensors/prepare.js +10 -0
  9. package/dist/src/commands/sensors/result-cache.js +278 -0
  10. package/dist/src/commands/sensors/result.js +31 -12
  11. package/dist/src/commands/sensors/run.js +265 -8
  12. package/dist/src/commands/sensors/status.js +66 -7
  13. package/dist/src/commands/watch/init.js +3 -15
  14. package/dist/src/core/awm-gitignore.js +211 -0
  15. package/dist/src/core/changes/receipt.js +219 -9
  16. package/dist/src/core/diagnostics/checks.js +11 -0
  17. package/dist/tests/commands/change/new.test.js +1 -1
  18. package/dist/tests/commands/change/validate-unmeasured.test.js +246 -0
  19. package/dist/tests/commands/preflight/awm-gitignore.test.js +52 -0
  20. package/dist/tests/commands/preflight/sensors-inputs.test.js +110 -0
  21. package/dist/tests/commands/sensors/compatibility/contract.test.js +60 -0
  22. package/dist/tests/commands/sensors/compatibility/manifest.test.js +94 -0
  23. package/dist/tests/commands/sensors/init-gitignore.test.js +59 -0
  24. package/dist/tests/commands/sensors/inputs-digest.test.js +373 -0
  25. package/dist/tests/commands/sensors/result-cache.test.js +352 -0
  26. package/dist/tests/commands/sensors/run-cache.test.js +434 -0
  27. package/dist/tests/commands/sensors/run-slice.test.js +171 -0
  28. package/dist/tests/commands/sensors/status-inputs.test.js +271 -0
  29. package/dist/tests/commands/watch/init-gitignore.test.js +34 -0
  30. package/dist/tests/commands/watch/track-freeze.test.js +11 -7
  31. package/dist/tests/commands/watch/watch-init.test.js +3 -1
  32. package/dist/tests/core/awm-gitignore.test.js +58 -0
  33. package/dist/tests/core/changes/receipt.test.js +261 -2
  34. package/dist/tests/core/diagnostics/awm-gitignore.test.js +101 -0
  35. package/dist/tests/core/ledger/gitignore.test.js +6 -1
  36. package/dist/tests/structural/sensor-execution-optimization-docs-contract.test.js +102 -0
  37. package/dist/tests/structural/sensor-slice-decision-contract.test.js +63 -0
  38. package/package.json +2 -1
  39. package/prebuilds/darwin-arm64/secure_fs.node +0 -0
  40. package/prebuilds/darwin-x64/secure_fs.node +0 -0
  41. package/prebuilds/win32-arm64/secure_fs.node +0 -0
  42. package/prebuilds/win32-x64/secure_fs.node +0 -0
@@ -0,0 +1,271 @@
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 status_1 = require("../../../src/commands/sensors/status");
10
+ /**
11
+ * RF-3.4 — status surfaces a non-blocking warning when a configured enabled
12
+ * sensor lacks effective `inputs` (not cacheable). Warnings must not degrade
13
+ * `overall`, and opt-out / native-gate must stay silent.
14
+ */
15
+ describe('computeSensorStatus inputs warnings (RF-3.4)', () => {
16
+ let tmpDir;
17
+ let previousHome;
18
+ let home;
19
+ beforeEach(() => {
20
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-'));
21
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-home-'));
22
+ previousHome = process.env.AWM_HOME;
23
+ process.env.AWM_HOME = home;
24
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
25
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
26
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
27
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
28
+ });
29
+ afterEach(() => {
30
+ if (previousHome === undefined)
31
+ delete process.env.AWM_HOME;
32
+ else
33
+ process.env.AWM_HOME = previousHome;
34
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
35
+ fs_1.default.rmSync(home, { recursive: true, force: true });
36
+ });
37
+ function writeOnlyProject(sensors) {
38
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
39
+ schemaVersion: 3,
40
+ mode: 'project-sensors',
41
+ sensors,
42
+ }));
43
+ }
44
+ function projectSensor(extra = {}) {
45
+ return {
46
+ source: 'project',
47
+ enabled: true,
48
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
49
+ formatter: 'exit-code',
50
+ ...extra,
51
+ };
52
+ }
53
+ it('warns when an enabled sensor lacks effective inputs (not cacheable)', async () => {
54
+ writeOnlyProject({ custom: projectSensor() });
55
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
56
+ expect(result.overall).toBe('READY');
57
+ expect(result.warnings).toEqual(expect.arrayContaining([
58
+ expect.objectContaining({
59
+ sensor: 'custom',
60
+ detail: expect.stringMatching(/not cacheable|no effective inputs|lacks inputs/i),
61
+ }),
62
+ ]));
63
+ });
64
+ it('does not warn when effective inputs are present', async () => {
65
+ writeOnlyProject({ custom: projectSensor({ inputs: ['**/*.ts'] }) });
66
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
67
+ expect(result.overall).toBe('READY');
68
+ expect(result.warnings ?? []).toEqual([]);
69
+ });
70
+ it('does not warn for disabled sensors that lack inputs', async () => {
71
+ writeOnlyProject({ custom: projectSensor({ enabled: false }) });
72
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
73
+ expect(result.warnings ?? []).toEqual([]);
74
+ expect(result.checks.custom).toMatchObject({ ok: true, detail: expect.stringMatching(/disabled/i) });
75
+ });
76
+ it.each([
77
+ ['native-gate', 'CI is authoritative.'],
78
+ ['opt-out', 'No local gate.'],
79
+ ])('emits no spurious warnings for v3 %s', async (mode, reason) => {
80
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
81
+ schemaVersion: 3, mode, reason,
82
+ }));
83
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
84
+ expect(result.mode).toBe(mode);
85
+ expect(result.warnings ?? []).toEqual([]);
86
+ });
87
+ it('uses pack-variant inputs as effective inputs (no warning when only the pack declares them)', async () => {
88
+ const registry = path_1.default.join(home, 'registries', 'baseline');
89
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
90
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([
91
+ { name: 'baseline', remote: 'https://example.test/baseline.git' },
92
+ ]));
93
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
94
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
95
+ coverage: {
96
+ schemaVersion: 1,
97
+ classes: {
98
+ lint: {
99
+ description: 'lint',
100
+ detectors: [{ sensor: 'lint' }],
101
+ remedy: { summary: 'fix', command: 'awm sensors init' },
102
+ },
103
+ },
104
+ },
105
+ sensors: {
106
+ lint: {
107
+ applicability: { allFiles: ['package.json'] },
108
+ variants: [{
109
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10 <11',
110
+ requirements: {
111
+ tool: 'eslint', toolRange: '>=10 <11',
112
+ runtime: 'node', runtimeRange: '>=0',
113
+ },
114
+ assets: [], formatter: 'eslint-llm',
115
+ probe: { kind: 'package-script-present', script: 'lint' },
116
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
117
+ inputs: ['src/**/*.ts'],
118
+ }],
119
+ },
120
+ },
121
+ }));
122
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
123
+ scripts: { lint: 'eslint .' },
124
+ devDependencies: { eslint: '^10.0.0' },
125
+ }));
126
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
127
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
128
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
129
+ schemaVersion: 2, pack: 'js-ts',
130
+ sensors: {
131
+ lint: {
132
+ enabled: true, variantId: 'eslint-10',
133
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
134
+ assets: [],
135
+ initializedCompatibility: {
136
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
137
+ toolVersion: '10.0.0', runtimeVersion: process.versions.node,
138
+ certifiedRange: '>=10 <11', evidence: [],
139
+ },
140
+ },
141
+ },
142
+ }));
143
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
144
+ expect(result.checks.lint).toMatchObject({ ok: true });
145
+ expect(result.warnings ?? []).toEqual([]);
146
+ });
147
+ it('warns for pack-bound enabled sensors when neither project nor pack variant provides inputs', async () => {
148
+ const registry = path_1.default.join(home, 'registries', 'baseline');
149
+ fs_1.default.mkdirSync(path_1.default.join(registry, 'sensor-packs', 'js-ts'), { recursive: true });
150
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([
151
+ { name: 'baseline', remote: 'https://example.test/baseline.git' },
152
+ ]));
153
+ fs_1.default.writeFileSync(path_1.default.join(registry, 'sensor-packs', 'js-ts', 'pack.json'), JSON.stringify({
154
+ schemaVersion: 2, name: 'js-ts', description: 'fixture', detects: ['package.json'],
155
+ coverage: {
156
+ schemaVersion: 1,
157
+ classes: {
158
+ lint: {
159
+ description: 'lint',
160
+ detectors: [{ sensor: 'lint' }],
161
+ remedy: { summary: 'fix', command: 'awm sensors init' },
162
+ },
163
+ },
164
+ },
165
+ sensors: {
166
+ lint: {
167
+ applicability: { allFiles: ['package.json'] },
168
+ variants: [{
169
+ id: 'eslint-10', priority: 10, certifiedRange: '>=10 <11',
170
+ requirements: {
171
+ tool: 'eslint', toolRange: '>=10 <11',
172
+ runtime: 'node', runtimeRange: '>=0',
173
+ },
174
+ assets: [], formatter: 'eslint-llm',
175
+ probe: { kind: 'package-script-present', script: 'lint' },
176
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
177
+ }],
178
+ },
179
+ },
180
+ }));
181
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'package.json'), JSON.stringify({
182
+ scripts: { lint: 'eslint .' },
183
+ devDependencies: { eslint: '^10.0.0' },
184
+ }));
185
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', 'eslint'), { recursive: true });
186
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', 'eslint', 'package.json'), JSON.stringify({ version: '10.0.0' }));
187
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
188
+ schemaVersion: 2, pack: 'js-ts',
189
+ sensors: {
190
+ lint: {
191
+ enabled: true, variantId: 'eslint-10',
192
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
193
+ assets: [],
194
+ initializedCompatibility: {
195
+ state: 'certified', reason: 'range-and-probe', variantId: 'eslint-10',
196
+ toolVersion: '10.0.0', runtimeVersion: process.versions.node,
197
+ certifiedRange: '>=10 <11', evidence: [],
198
+ },
199
+ },
200
+ },
201
+ }));
202
+ const result = await (0, status_1.computeSensorStatus)(tmpDir);
203
+ expect(result.overall).toBe('READY');
204
+ expect(result.warnings).toEqual(expect.arrayContaining([
205
+ expect.objectContaining({
206
+ sensor: 'lint',
207
+ detail: expect.stringMatching(/not cacheable|no effective inputs|lacks inputs/i),
208
+ }),
209
+ ]));
210
+ });
211
+ });
212
+ describe('awm sensors status CLI renders inputs warnings (RF-3.4)', () => {
213
+ let tmpDir;
214
+ let previousHome;
215
+ let home;
216
+ let previousCwd;
217
+ beforeEach(() => {
218
+ previousCwd = process.cwd();
219
+ tmpDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-cli-'));
220
+ home = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-status-inputs-cli-home-'));
221
+ previousHome = process.env.AWM_HOME;
222
+ process.env.AWM_HOME = home;
223
+ fs_1.default.writeFileSync(path_1.default.join(home, 'registries.json'), JSON.stringify([]));
224
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, 'node_modules', '.bin'), { recursive: true });
225
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, 'node_modules', '.bin', 'eslint'), '');
226
+ fs_1.default.mkdirSync(path_1.default.join(tmpDir, '.awm'), { recursive: true });
227
+ fs_1.default.writeFileSync(path_1.default.join(tmpDir, '.awm', 'sensors.json'), JSON.stringify({
228
+ schemaVersion: 3,
229
+ mode: 'project-sensors',
230
+ sensors: {
231
+ custom: {
232
+ source: 'project',
233
+ enabled: true,
234
+ command: { executable: 'eslint', resolution: 'node-modules-bin', args: ['.'] },
235
+ formatter: 'exit-code',
236
+ },
237
+ },
238
+ }));
239
+ process.chdir(tmpDir);
240
+ });
241
+ afterEach(() => {
242
+ process.chdir(previousCwd);
243
+ if (previousHome === undefined)
244
+ delete process.env.AWM_HOME;
245
+ else
246
+ process.env.AWM_HOME = previousHome;
247
+ fs_1.default.rmSync(tmpDir, { recursive: true, force: true });
248
+ fs_1.default.rmSync(home, { recursive: true, force: true });
249
+ });
250
+ it('prints warning text in human-readable status output without failing READY', async () => {
251
+ const { Command } = require('commander');
252
+ const { registerSensorsCommand } = require('../../../src/commands/sensors/index');
253
+ const program = new Command();
254
+ program.exitOverride();
255
+ registerSensorsCommand(program);
256
+ const output = jest.spyOn(console, 'log').mockImplementation(() => undefined);
257
+ const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => undefined));
258
+ try {
259
+ await program.parseAsync(['node', 'awm', 'sensors', 'status']);
260
+ const rendered = output.mock.calls.flat().join('\n');
261
+ expect(rendered).toMatch(/READY/);
262
+ expect(rendered).toMatch(/custom/);
263
+ expect(rendered).toMatch(/not cacheable|no effective inputs|lacks inputs/i);
264
+ expect(exitSpy).not.toHaveBeenCalled();
265
+ }
266
+ finally {
267
+ output.mockRestore();
268
+ exitSpy.mockRestore();
269
+ }
270
+ });
271
+ });
@@ -0,0 +1,34 @@
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 init_1 = require("../../../src/commands/watch/init");
11
+ const awm_gitignore_1 = require("../../../src/core/awm-gitignore");
12
+ const git_1 = require("../../../src/core/tracks/git");
13
+ describe('initWatch managed awm gitignore (GI-2/GI-10)', () => {
14
+ let repo;
15
+ beforeEach(() => {
16
+ repo = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-watch-gi-'));
17
+ (0, child_process_1.execFileSync)('git', ['init'], { cwd: repo, stdio: 'pipe' });
18
+ fs_1.default.writeFileSync(path_1.default.join(repo, 'package.json'), JSON.stringify({ scripts: { test: 'jest' } }));
19
+ });
20
+ afterEach(() => {
21
+ fs_1.default.rmSync(repo, { recursive: true, force: true });
22
+ });
23
+ it('writes the managed block instead of a bare .awm/ allow-nothing rule', () => {
24
+ (0, init_1.initWatch)(repo, 'rama');
25
+ const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
26
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
27
+ expect(gi).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
28
+ expect(gi).toContain('!.awm/sensors.json');
29
+ expect(gi).toContain('!.awm/profile.json');
30
+ const lines = gi.split('\n').map((l) => l.trim()).filter(Boolean);
31
+ expect(lines.filter((l) => l === '.awm' || l === '.awm/')).toEqual([]);
32
+ expect((0, git_1.isAwmGitignored)(repo)).toBe(true);
33
+ });
34
+ });
@@ -301,13 +301,10 @@ describe('Supervisor.tick() — freeze de un track individual (R5.2/R6.3)', () =
301
301
  }
302
302
  function setUpGreenTrack() {
303
303
  initCompactWatch(repo, BRANCH);
304
- // `initWatch` ESCRIBE `.gitignore` (`.awm/`) pero no lo commitea — un
305
- // `.gitignore` sin trackear es en sí mismo un path "untracked" para
306
- // `git status --porcelain` (R6.4's chequeo de limpieza no distingue
307
- // "sucio de verdad" de "el propio bootstrap del journal"). Sin este
308
- // commit, `isWorktreeClean` nunca sería `true` y el freeze jamás
309
- // convergería — no por un bug del driver, sino por un repo de test
310
- // que nunca terminó su propio setup.
304
+ // `initWatch` upserts the managed `# AWM:GITIGNORE` block but does not
305
+ // commit it — an untracked/modified `.gitignore` keeps the worktree
306
+ // dirty for R6.4's clean check. Commit the bootstrap side-effect so
307
+ // freeze can converge.
311
308
  git(repo, 'add', '.gitignore');
312
309
  git(repo, 'commit', '-qm', 'gitignore .awm');
313
310
  const s = (0, store_1.readJournal)(repo, BRANCH).state;
@@ -526,6 +523,13 @@ describe('loop end-to-end: plan (reconcileTracks) + track (Supervisor.tick) cier
526
523
  test('reconcileTracks real (plan) + Supervisor.tick real (track), en loop contra dos worktrees reales, cierran request -> drenaje -> persist -> observación -> FROZEN', async () => {
527
524
  // --- lado TRACK: registrar el trabajo y drenarlo con un Supervisor REAL ---
528
525
  initCompactWatch(trackWorktree, TRACK_BRANCH);
526
+ // `initWatch` migrates the seeded `.awm/` ignore into the managed
527
+ // `.awm/*` allowlist block. That (1) dirties `.gitignore` and (2)
528
+ // un-ignores `.awm/sensors.json`, which then shows up as untracked.
529
+ // Both must be committed or R6.4's clean-worktree gate never passes
530
+ // and this loop times out (same class of fixture bug as setUpGreenTrack).
531
+ git(trackWorktree, 'add', '.gitignore', '.awm/sensors.json');
532
+ git(trackWorktree, 'commit', '-qm', 'awm gitignore + sensors');
529
533
  const ts0 = (0, store_1.readJournal)(trackWorktree, TRACK_BRANCH).state;
530
534
  ts0.trackContext = { trackId: 'a', taskIds: ['T1'], planDigest: '', baseSha, planJournalId: 'j-plan' };
531
535
  (0, store_1.writeJournal)(trackWorktree, TRACK_BRANCH, ts0);
@@ -154,7 +154,9 @@ describe('watch --init: plan-vs-repo mecanico', () => {
154
154
  const out = (0, init_1.initWatch)(repo, 'rama');
155
155
  expect(out.requiredVerifiers).toEqual(['test']);
156
156
  expect((0, store_1.readJournal)(repo, 'rama').state.requiredVerifiers).toEqual(['test']);
157
- expect(fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8')).toContain('.awm/');
157
+ const gi = fs_1.default.readFileSync(path_1.default.join(repo, '.gitignore'), 'utf8');
158
+ expect(gi).toContain('# AWM:GITIGNORE:BEGIN');
159
+ expect(gi).toContain('.awm/*');
158
160
  expect(() => (0, init_1.initWatch)(repo, 'rama')).not.toThrow(); // idempotente
159
161
  });
160
162
  test('no escribe .gitignore cuando es un symlink', () => {
@@ -0,0 +1,58 @@
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 awm_gitignore_1 = require("../../src/core/awm-gitignore");
10
+ describe('awm-gitignore managed block', () => {
11
+ let tmp;
12
+ beforeEach(() => {
13
+ tmp = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'awm-gi-'));
14
+ });
15
+ afterEach(() => {
16
+ fs_1.default.rmSync(tmp, { recursive: true, force: true });
17
+ });
18
+ it('renders the canonical managed block with versioned allowlist entries', () => {
19
+ expect((0, awm_gitignore_1.renderManagedBlock)()).toBe([
20
+ awm_gitignore_1.AWM_GITIGNORE_BEGIN,
21
+ '.awm/*',
22
+ '!.awm/sensors.json',
23
+ '!.awm/sensors.baseline.json',
24
+ '!.awm/profile.json',
25
+ '!.awm/context-budget.json',
26
+ '!.awm/context/',
27
+ '.awm/context/*',
28
+ '!.awm/context/index.json',
29
+ awm_gitignore_1.AWM_GITIGNORE_END,
30
+ '',
31
+ ].join('\n'));
32
+ });
33
+ it('creates .gitignore with the managed block and is idempotent', () => {
34
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
35
+ const first = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
36
+ expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
37
+ expect(first).toContain(awm_gitignore_1.AWM_GITIGNORE_END);
38
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
39
+ expect(fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8')).toBe(first);
40
+ });
41
+ it('migrates bare .awm/ and .awm lines into the managed block without dropping unrelated rules', () => {
42
+ fs_1.default.writeFileSync(path_1.default.join(tmp, '.gitignore'), 'node_modules\n.awm/\ndist\n');
43
+ (0, awm_gitignore_1.upsertAwmGitignore)(tmp);
44
+ const text = fs_1.default.readFileSync(path_1.default.join(tmp, '.gitignore'), 'utf8');
45
+ expect(text).toContain('node_modules');
46
+ expect(text).toContain('dist');
47
+ expect(text).toContain(awm_gitignore_1.AWM_GITIGNORE_BEGIN);
48
+ const outside = text.slice(0, text.indexOf(awm_gitignore_1.AWM_GITIGNORE_BEGIN));
49
+ expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm/');
50
+ expect(outside.split('\n').map((l) => l.trim()).filter(Boolean)).not.toContain('.awm');
51
+ });
52
+ it('refuses a symlink .gitignore', () => {
53
+ const target = path_1.default.join(tmp, 'ignore-target');
54
+ fs_1.default.writeFileSync(target, 'x\n');
55
+ fs_1.default.symlinkSync(target, path_1.default.join(tmp, '.gitignore'));
56
+ expect(() => (0, awm_gitignore_1.upsertAwmGitignore)(tmp)).toThrow(/symlink/i);
57
+ });
58
+ });