agentic-workflow-manager 6.5.2 → 8.0.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/scripts/sensor-support-matrix.js +103 -0
- package/dist/scripts/support-matrix.js +40 -17
- package/dist/src/commands/init.js +1 -3
- package/dist/src/commands/ledger/index.js +6 -0
- package/dist/src/commands/preflight/checks.js +4 -4
- package/dist/src/commands/preflight/index.js +2 -2
- package/dist/src/commands/registry/add.js +2 -2
- package/dist/src/commands/registry/index.js +5 -0
- package/dist/src/commands/registry/status.js +7 -0
- package/dist/src/commands/sensors/compatibility/contract.js +261 -0
- package/dist/src/commands/sensors/compatibility/discovery.js +207 -0
- package/dist/src/commands/sensors/compatibility/live.js +59 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +197 -0
- package/dist/src/commands/sensors/compatibility/materialize.js +126 -0
- package/dist/src/commands/sensors/compatibility/pack-source.js +78 -0
- package/dist/src/commands/sensors/compatibility/probe.js +47 -0
- package/dist/src/commands/sensors/compatibility/resolve.js +96 -0
- package/dist/src/commands/sensors/compatibility/types.js +2 -0
- package/dist/src/commands/sensors/coverage/contract.js +0 -50
- package/dist/src/commands/sensors/coverage/empirical.js +131 -0
- package/dist/src/commands/sensors/coverage/evaluate.js +85 -7
- package/dist/src/commands/sensors/coverage/evidence.js +5 -1
- package/dist/src/commands/sensors/coverage/index.js +44 -7
- package/dist/src/commands/sensors/coverage/render.js +214 -14
- package/dist/src/commands/sensors/coverage/resolve.js +24 -38
- package/dist/src/commands/sensors/exec.js +150 -9
- package/dist/src/commands/sensors/index.js +18 -6
- package/dist/src/commands/sensors/init.js +92 -6
- package/dist/src/commands/sensors/run.js +71 -0
- package/dist/src/commands/sensors/status.js +48 -4
- package/dist/src/commands/sync.js +7 -0
- package/dist/src/core/bundles.js +38 -3
- package/dist/src/core/init/steps.js +7 -4
- package/dist/src/core/ledger/scan.js +228 -0
- package/dist/src/core/ledger/store.js +89 -25
- package/dist/src/core/ledger/types.js +54 -0
- package/dist/src/core/paths.js +10 -9
- package/dist/src/core/registries.js +134 -14
- package/dist/src/index.js +24 -1
- package/dist/tests/commands/ledger/index.test.js +11 -0
- package/dist/tests/commands/preflight/preflight.test.js +64 -64
- package/dist/tests/commands/registry/add.test.js +69 -2
- package/dist/tests/commands/registry/status.test.js +14 -0
- package/dist/tests/commands/sensors/compatibility/contract.test.js +98 -0
- package/dist/tests/commands/sensors/compatibility/discovery.test.js +119 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +68 -0
- package/dist/tests/commands/sensors/compatibility/materialize.test.js +53 -0
- package/dist/tests/commands/sensors/compatibility/pack-source.test.js +55 -0
- package/dist/tests/commands/sensors/compatibility/probe.test.js +27 -0
- package/dist/tests/commands/sensors/compatibility/resolve.test.js +75 -0
- package/dist/tests/commands/sensors/coverage/contract.test.js +8 -22
- package/dist/tests/commands/sensors/coverage/empirical.test.js +111 -0
- package/dist/tests/commands/sensors/coverage/evaluate.test.js +29 -0
- package/dist/tests/commands/sensors/coverage/evidence.test.js +9 -0
- package/dist/tests/commands/sensors/coverage/index.test.js +40 -13
- package/dist/tests/commands/sensors/coverage/render.test.js +187 -12
- package/dist/tests/commands/sensors/coverage/resolve.test.js +25 -4
- package/dist/tests/commands/sensors/exec-windows.test.js +92 -2
- package/dist/tests/commands/sensors/exec.test.js +67 -5
- package/dist/tests/commands/sensors/index.test.js +8 -0
- package/dist/tests/commands/sensors/init-pack-unavailable.test.js +12 -12
- package/dist/tests/commands/sensors/init.test.js +132 -48
- package/dist/tests/commands/sensors/run-inconclusive.test.js +9 -2
- package/dist/tests/commands/sensors/run-is-read-only.test.js +0 -0
- package/dist/tests/commands/sensors/run.test.js +66 -3
- package/dist/tests/commands/sensors/status-windows.test.js +7 -7
- package/dist/tests/commands/sensors/status.test.js +74 -23
- package/dist/tests/commands/update.test.js +1 -1
- package/dist/tests/core/bundles.test.js +37 -0
- package/dist/tests/core/discovery.test.js +4 -0
- package/dist/tests/core/init/orchestrator.test.js +5 -1
- package/dist/tests/core/init/steps.test.js +20 -4
- package/dist/tests/core/ledger/scan.test.js +217 -0
- package/dist/tests/core/ledger/store.test.js +108 -0
- package/dist/tests/core/profile-pins.test.js +2 -1
- package/dist/tests/core/project-skill-links.test.js +22 -0
- package/dist/tests/core/registries-capability.test.js +10 -0
- package/dist/tests/core/registries-sync.test.js +16 -0
- package/dist/tests/core/registries.test.js +129 -1
- package/dist/tests/core/registry-manifest.test.js +7 -0
- package/dist/tests/core/same-existing-path.test.js +35 -0
- package/dist/tests/core/sync-gates.test.js +2 -1
- package/dist/tests/integration/copilot-init-isolated.test.js +18 -0
- package/dist/tests/integration/sensor-compatibility.e2e.test.js +141 -0
- package/dist/tests/integration/sensor-coverage.e2e.test.js +35 -6
- package/dist/tests/integration/sensor-structured-run.e2e.test.js +127 -0
- package/dist/tests/structural/active-documentation.test.js +128 -0
- package/dist/tests/structural/async-entrypoint.test.js +15 -0
- package/dist/tests/structural/jest-environment-is-isolated.test.js +44 -0
- package/dist/tests/structural/r3-cli-major-version.test.js +37 -0
- package/dist/tests/structural/sensor-documentation-contract.test.js +77 -0
- package/dist/tests/structural/support-matrix-is-current.test.js +69 -2
- package/package.json +8 -2
|
@@ -47,56 +47,56 @@ const make = (o) => { const d = project(o); dirs.push(d); return d; };
|
|
|
47
47
|
afterAll(() => dirs.forEach(d => fs_1.default.rmSync(d, { recursive: true, force: true })));
|
|
48
48
|
const check = (r, id) => r.checks.find(c => c.id === id);
|
|
49
49
|
describe('preflight', () => {
|
|
50
|
-
it('reports not_configured when no sensor manifest exists', () => {
|
|
50
|
+
it('reports not_configured when no sensor manifest exists', async () => {
|
|
51
51
|
// The team-rollout case: a developer clones the repo and never runs
|
|
52
52
|
// `awm sensors init`. Today nothing notices until an unattended run is already
|
|
53
53
|
// in flight and every quality phase is consuming a gate that certifies nothing.
|
|
54
54
|
const dir = make();
|
|
55
|
-
const report = (0, checks_1.preflight)(dir);
|
|
55
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
56
56
|
expect(report.status).toBe('not_configured');
|
|
57
57
|
expect(check(report, 'manifest').ok).toBe(false);
|
|
58
58
|
});
|
|
59
|
-
it('keeps not_configured and degraded apart', () => {
|
|
59
|
+
it('keeps not_configured and degraded apart', async () => {
|
|
60
60
|
// "You never set this up" and "you set it up and it broke" need different
|
|
61
61
|
// remedies. Collapsing them is how an absent check reads as a passing one.
|
|
62
62
|
const never = make();
|
|
63
63
|
const broken = make({
|
|
64
64
|
manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } }, // no local eslint
|
|
65
65
|
});
|
|
66
|
-
expect((0, checks_1.preflight)(never).status).toBe('not_configured');
|
|
67
|
-
expect((0, checks_1.preflight)(broken).status).toBe('degraded');
|
|
66
|
+
expect((await (0, checks_1.preflight)(never)).status).toBe('not_configured');
|
|
67
|
+
expect((await (0, checks_1.preflight)(broken)).status).toBe('degraded');
|
|
68
68
|
});
|
|
69
|
-
it('is ready when the declared sensors can actually run', () => {
|
|
69
|
+
it('is ready when the declared sensors can actually run', async () => {
|
|
70
70
|
const dir = make({
|
|
71
71
|
manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
|
|
72
72
|
bins: ['eslint'],
|
|
73
73
|
files: ['package.json'],
|
|
74
74
|
});
|
|
75
|
-
const report = (0, checks_1.preflight)(dir);
|
|
75
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
76
76
|
expect(report.status).toBe('ready');
|
|
77
77
|
expect((0, preflight_1.exitCodeFor)(report)).toBe(0);
|
|
78
78
|
});
|
|
79
|
-
it('catches a sensor whose tool is not installed locally', () => {
|
|
79
|
+
it('catches a sensor whose tool is not installed locally', async () => {
|
|
80
80
|
// This is the check that existed and nothing in the flow was calling.
|
|
81
81
|
const dir = make({
|
|
82
82
|
manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
|
|
83
83
|
files: ['package.json'],
|
|
84
84
|
});
|
|
85
|
-
const report = (0, checks_1.preflight)(dir);
|
|
85
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
86
86
|
expect(report.status).toBe('degraded');
|
|
87
87
|
expect(check(report, 'tools').detail).toContain('eslint');
|
|
88
88
|
});
|
|
89
|
-
it('accepts a deliberate opt-out, but only when it is written down', () => {
|
|
89
|
+
it('accepts a deliberate opt-out, but only when it is written down', async () => {
|
|
90
90
|
// A repo may legitimately have no sensors — it just has to SAY so in a committed
|
|
91
91
|
// file, so "we decided not to gate this" cannot be mistaken for "nobody set it up".
|
|
92
92
|
const optedOut = make({
|
|
93
93
|
manifest: { pack: 'generic', sensors: { security: { cmd: 'semgrep .', enabled: false } } },
|
|
94
94
|
});
|
|
95
|
-
const report = (0, checks_1.preflight)(optedOut);
|
|
95
|
+
const report = await (0, checks_1.preflight)(optedOut);
|
|
96
96
|
expect(report.status).toBe('ready');
|
|
97
97
|
expect(check(report, 'manifest').detail).toContain('opt-out');
|
|
98
98
|
});
|
|
99
|
-
it('flags a manifest with zero sensor entries as degraded, distinct from a deliberate opt-out', () => {
|
|
99
|
+
it('flags a manifest with zero sensor entries as degraded, distinct from a deliberate opt-out', async () => {
|
|
100
100
|
// Genuinely different manifest shape from the opt-out test above: no sensor
|
|
101
101
|
// NAMES at all, vs. an opt-out which lists every known sensor explicitly with
|
|
102
102
|
// `enabled: false`. This is the honest-floor case from init.ts — the registry
|
|
@@ -104,14 +104,14 @@ describe('preflight', () => {
|
|
|
104
104
|
const noPack = make({
|
|
105
105
|
manifest: { pack: 'python', sensors: {} },
|
|
106
106
|
});
|
|
107
|
-
const report = (0, checks_1.preflight)(noPack);
|
|
107
|
+
const report = await (0, checks_1.preflight)(noPack);
|
|
108
108
|
expect(report.status).toBe('degraded');
|
|
109
109
|
expect(check(report, 'manifest').ok).toBe(false);
|
|
110
110
|
expect(check(report, 'manifest').detail).not.toContain('opt-out');
|
|
111
111
|
expect(check(report, 'manifest').detail).toContain('python');
|
|
112
112
|
expect(check(report, 'manifest').remedy).toContain('python');
|
|
113
113
|
});
|
|
114
|
-
it('flags the tools check as failing (not "0/0 runnable") for a manifest with zero sensor entries', () => {
|
|
114
|
+
it('flags the tools check as failing (not "0/0 runnable") for a manifest with zero sensor entries', async () => {
|
|
115
115
|
// Regression for Finding 6: `checkTools` independently inspects
|
|
116
116
|
// `status.checks`, which is also `{}` for a zero-sensor manifest —
|
|
117
117
|
// `Object.entries({}).filter(...)` is vacuously `[]`, so before the fix this
|
|
@@ -122,34 +122,34 @@ describe('preflight', () => {
|
|
|
122
122
|
const noPack = make({
|
|
123
123
|
manifest: { pack: 'python', sensors: {} },
|
|
124
124
|
});
|
|
125
|
-
const report = (0, checks_1.preflight)(noPack);
|
|
125
|
+
const report = await (0, checks_1.preflight)(noPack);
|
|
126
126
|
expect(check(report, 'tools').ok).toBe(false);
|
|
127
127
|
expect(report.status).toBe('degraded');
|
|
128
128
|
});
|
|
129
|
-
it('flags a manifest stuck on generic while the tree has a real stack', () => {
|
|
129
|
+
it('flags a manifest stuck on generic while the tree has a real stack', async () => {
|
|
130
130
|
// The gate would run, report green, and have checked almost nothing.
|
|
131
131
|
const dir = make({
|
|
132
132
|
manifest: { pack: 'generic', sensors: {} },
|
|
133
133
|
files: ['package.json'],
|
|
134
134
|
});
|
|
135
|
-
const report = (0, checks_1.preflight)(dir);
|
|
135
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
136
136
|
expect(report.status).toBe('degraded');
|
|
137
137
|
expect(check(report, 'pack').ok).toBe(false);
|
|
138
138
|
});
|
|
139
|
-
it('flags a repo with no context contract at all', () => {
|
|
139
|
+
it('flags a repo with no context contract at all', async () => {
|
|
140
140
|
const dir = make({
|
|
141
141
|
context: [],
|
|
142
142
|
manifest: { pack: 'generic', sensors: {} },
|
|
143
143
|
});
|
|
144
|
-
expect(check((0, checks_1.preflight)(dir), 'context').ok).toBe(false);
|
|
144
|
+
expect(check(await (0, checks_1.preflight)(dir), 'context').ok).toBe(false);
|
|
145
145
|
});
|
|
146
|
-
it('treats an unparseable manifest as a failure, not as absent', () => {
|
|
146
|
+
it('treats an unparseable manifest as a failure, not as absent', async () => {
|
|
147
147
|
const dir = make({ manifest: '{ not json' });
|
|
148
|
-
const report = (0, checks_1.preflight)(dir);
|
|
148
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
149
149
|
expect(report.status).toBe('degraded');
|
|
150
150
|
expect(check(report, 'manifest').detail).toContain('not valid JSON');
|
|
151
151
|
});
|
|
152
|
-
it('exits non-zero for anything but ready, so the caller need not parse JSON', () => {
|
|
152
|
+
it('exits non-zero for anything but ready, so the caller need not parse JSON', async () => {
|
|
153
153
|
// Unlike `awm sensors run` — which exits 0 on not_certified because exit 2 blocks
|
|
154
154
|
// Claude Code hooks — preflight is never a hook, so the verdict rides the exit code
|
|
155
155
|
// instead of depending on every agent remembering to read a field.
|
|
@@ -187,74 +187,74 @@ describe('preflight', () => {
|
|
|
187
187
|
fs_1.default.writeFileSync(f, '#!/bin/sh\nexit 0\n');
|
|
188
188
|
fs_1.default.chmodSync(f, 0o755);
|
|
189
189
|
}
|
|
190
|
-
it('reports github + gh available, and does not affect status', () => {
|
|
190
|
+
it('reports github + gh available, and does not affect status', async () => {
|
|
191
191
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
192
192
|
gitRepo(dir, 'git@github.com:kodria/agentic-workflow.git');
|
|
193
193
|
installOnPath('gh');
|
|
194
|
-
const report = (0, checks_1.preflight)(dir);
|
|
194
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
195
195
|
expect(check(report, 'host').ok).toBe(true);
|
|
196
196
|
expect(check(report, 'host').detail).toBe('github detected, gh available');
|
|
197
197
|
expect(report.status).toBe('ready');
|
|
198
198
|
});
|
|
199
|
-
it('is still ok:true (advisory only) when gitlab is detected but glab is not on PATH, and status stays ready', () => {
|
|
199
|
+
it('is still ok:true (advisory only) when gitlab is detected but glab is not on PATH, and status stays ready', async () => {
|
|
200
200
|
// The only thing "wrong" in this fixture is the missing `glab` — proving the
|
|
201
201
|
// advisory contract: it must not drag an otherwise-clean repo to `degraded`.
|
|
202
202
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
203
203
|
gitRepo(dir, 'https://gitlab.com/kodria/agentic-workflow.git');
|
|
204
204
|
// PATH aislado y vacio => glab no resuelve.
|
|
205
|
-
const report = (0, checks_1.preflight)(dir);
|
|
205
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
206
206
|
expect(check(report, 'host').ok).toBe(true);
|
|
207
207
|
expect(check(report, 'host').detail).toContain('glab not on PATH');
|
|
208
208
|
expect(check(report, 'host').remedy).toContain('glab');
|
|
209
209
|
expect(report.status).toBe('ready');
|
|
210
210
|
});
|
|
211
|
-
it('handles no origin remote gracefully — no throw, ok:true, minimal detail', () => {
|
|
211
|
+
it('handles no origin remote gracefully — no throw, ok:true, minimal detail', async () => {
|
|
212
212
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
213
213
|
// Not a git repo at all — the common case for `execFileSync` failing here.
|
|
214
|
-
const report = (0, checks_1.preflight)(dir);
|
|
214
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
215
215
|
expect(check(report, 'host').ok).toBe(true);
|
|
216
216
|
expect(check(report, 'host').detail).toBe('no git remote detected — PR/MR automation not applicable');
|
|
217
217
|
expect(check(report, 'host').remedy).toBeUndefined();
|
|
218
218
|
expect(report.status).toBe('ready');
|
|
219
219
|
});
|
|
220
|
-
it('handles a git repo with no origin remote configured gracefully', () => {
|
|
220
|
+
it('handles a git repo with no origin remote configured gracefully', async () => {
|
|
221
221
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
222
222
|
gitRepo(dir); // git init, no remote
|
|
223
|
-
const report = (0, checks_1.preflight)(dir);
|
|
223
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
224
224
|
expect(check(report, 'host').ok).toBe(true);
|
|
225
225
|
expect(check(report, 'host').detail).toContain('no git remote detected');
|
|
226
226
|
});
|
|
227
|
-
it('does not overclaim support for an unrecognized host', () => {
|
|
227
|
+
it('does not overclaim support for an unrecognized host', async () => {
|
|
228
228
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
229
229
|
gitRepo(dir, 'git@bitbucket.org:kodria/agentic-workflow.git');
|
|
230
|
-
const report = (0, checks_1.preflight)(dir);
|
|
230
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
231
231
|
expect(check(report, 'host').ok).toBe(true);
|
|
232
232
|
expect(check(report, 'host').detail).toContain('not recognized');
|
|
233
233
|
expect(report.status).toBe('ready');
|
|
234
234
|
});
|
|
235
|
-
it('does not misclassify a GitHub Enterprise host whose repo NAME contains "gitlab"', () => {
|
|
235
|
+
it('does not misclassify a GitHub Enterprise host whose repo NAME contains "gitlab"', async () => {
|
|
236
236
|
// The bug: a bare `remote.includes('gitlab')` matches the full remote URL
|
|
237
237
|
// string, so an org/repo name containing "gitlab" false-positives even though
|
|
238
238
|
// the actual host is unrelated. Hostname must be extracted first and matched
|
|
239
239
|
// in isolation.
|
|
240
240
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
241
241
|
gitRepo(dir, 'git@github.enterprise.internal:kodria/gitlab-migration-tool.git');
|
|
242
|
-
const report = (0, checks_1.preflight)(dir);
|
|
242
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
243
243
|
expect(check(report, 'host').ok).toBe(true);
|
|
244
244
|
expect(check(report, 'host').detail).toContain('not recognized');
|
|
245
245
|
expect(report.status).toBe('ready');
|
|
246
246
|
});
|
|
247
|
-
it('does not misclassify a non-GitHub host whose repo NAME contains "github"', () => {
|
|
247
|
+
it('does not misclassify a non-GitHub host whose repo NAME contains "github"', async () => {
|
|
248
248
|
// Same class of bug on the github side: "something-github-tool" is a repo
|
|
249
249
|
// name, not the host.
|
|
250
250
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
251
251
|
gitRepo(dir, 'https://example.com/kodria/something-github-tool.git');
|
|
252
|
-
const report = (0, checks_1.preflight)(dir);
|
|
252
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
253
253
|
expect(check(report, 'host').ok).toBe(true);
|
|
254
254
|
expect(check(report, 'host').detail).toContain('not recognized');
|
|
255
255
|
expect(report.status).toBe('ready');
|
|
256
256
|
});
|
|
257
|
-
it('does not misclassify a GitHub Enterprise host whose SSH USERNAME is "gitlab"', () => {
|
|
257
|
+
it('does not misclassify a GitHub Enterprise host whose SSH USERNAME is "gitlab"', async () => {
|
|
258
258
|
// The related bug: the scheme-based regex captured everything between
|
|
259
259
|
// `scheme://` and the first `/`, including `userinfo@` — so an SSH username
|
|
260
260
|
// of "gitlab" leaked into the matched "host" string and false-positived the
|
|
@@ -269,25 +269,25 @@ describe('preflight', () => {
|
|
|
269
269
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
270
270
|
gitRepo(dir, 'ssh://gitlab@github.company-internal.com:22/team/repo.git');
|
|
271
271
|
// PATH real: estos casos no dependen de gh/glab.
|
|
272
|
-
const report = (0, checks_1.preflight)(dir);
|
|
272
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
273
273
|
expect(check(report, 'host').ok).toBe(true);
|
|
274
274
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
275
275
|
expect(check(report, 'host').detail).not.toContain('gitlab');
|
|
276
276
|
expect(report.status).toBe('ready');
|
|
277
277
|
});
|
|
278
|
-
it('does not misclassify a host whose injected credential/token contains "gitlab"', () => {
|
|
278
|
+
it('does not misclassify a host whose injected credential/token contains "gitlab"', async () => {
|
|
279
279
|
// A realistic CI credential-injection remote:
|
|
280
280
|
// `git remote set-url origin https://x-access-token:$TOKEN@host/...`. If the
|
|
281
281
|
// token or password happens to contain "gitlab", it must not leak into the
|
|
282
282
|
// matched host either.
|
|
283
283
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
284
284
|
gitRepo(dir, 'https://user:gitlab@example-host.com/org/repo.git');
|
|
285
|
-
const report = (0, checks_1.preflight)(dir);
|
|
285
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
286
286
|
expect(check(report, 'host').ok).toBe(true);
|
|
287
287
|
expect(check(report, 'host').detail).toContain('not recognized');
|
|
288
288
|
expect(report.status).toBe('ready');
|
|
289
289
|
});
|
|
290
|
-
it('does not misclassify an SCP-style remote with a second "@" in it', () => {
|
|
290
|
+
it('does not misclassify an SCP-style remote with a second "@" in it', async () => {
|
|
291
291
|
// `user@host:path` shorthand has no scheme for `URL` to parse, so it falls
|
|
292
292
|
// back to a regex. A second "@" (e.g. a malformed/adversarial remote) must
|
|
293
293
|
// not let a bogus "host@evil"-shaped capture slip past the colon check —
|
|
@@ -295,42 +295,42 @@ describe('preflight', () => {
|
|
|
295
295
|
// falls through to "unrecognized" rather than misclassifying as gitlab.
|
|
296
296
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
297
297
|
gitRepo(dir, 'user@github.com@gitlab.evil:org/repo.git');
|
|
298
|
-
const report = (0, checks_1.preflight)(dir);
|
|
298
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
299
299
|
expect(check(report, 'host').ok).toBe(true);
|
|
300
300
|
expect(check(report, 'host').detail).toContain('not recognized');
|
|
301
301
|
expect(report.status).toBe('ready');
|
|
302
302
|
});
|
|
303
|
-
it('still detects github.com over HTTPS', () => {
|
|
303
|
+
it('still detects github.com over HTTPS', async () => {
|
|
304
304
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
305
305
|
gitRepo(dir, 'https://github.com/org/repo.git');
|
|
306
306
|
// PATH real: estos casos no dependen de gh/glab.
|
|
307
|
-
const report = (0, checks_1.preflight)(dir);
|
|
307
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
308
308
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
309
309
|
});
|
|
310
|
-
it('still detects github.com over SSH shorthand', () => {
|
|
310
|
+
it('still detects github.com over SSH shorthand', async () => {
|
|
311
311
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
312
312
|
gitRepo(dir, 'git@github.com:org/repo.git');
|
|
313
313
|
// PATH real: estos casos no dependen de gh/glab.
|
|
314
|
-
const report = (0, checks_1.preflight)(dir);
|
|
314
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
315
315
|
expect(check(report, 'host').detail).toContain('github detected');
|
|
316
316
|
});
|
|
317
|
-
it('still detects gitlab over HTTPS', () => {
|
|
317
|
+
it('still detects gitlab over HTTPS', async () => {
|
|
318
318
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
319
319
|
gitRepo(dir, 'https://gitlab.example.com/org/repo.git');
|
|
320
320
|
// PATH real: estos casos no dependen de gh/glab.
|
|
321
|
-
const report = (0, checks_1.preflight)(dir);
|
|
321
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
322
322
|
expect(check(report, 'host').detail).toContain('gitlab detected');
|
|
323
323
|
});
|
|
324
|
-
it('still detects gitlab over SSH shorthand', () => {
|
|
324
|
+
it('still detects gitlab over SSH shorthand', async () => {
|
|
325
325
|
const dir = make({ manifest: { pack: 'generic', sensors: { security: { enabled: false } } } });
|
|
326
326
|
gitRepo(dir, 'git@gitlab.example.com:org/repo.git');
|
|
327
327
|
// PATH real: estos casos no dependen de gh/glab.
|
|
328
|
-
const report = (0, checks_1.preflight)(dir);
|
|
328
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
329
329
|
expect(check(report, 'host').detail).toContain('gitlab detected');
|
|
330
330
|
});
|
|
331
331
|
});
|
|
332
332
|
describe('sensors-baseline check (advisory — never changes the exit code)', () => {
|
|
333
|
-
it('nudges toward `awm sensors baseline` when sensors are configured but no baseline exists', () => {
|
|
333
|
+
it('nudges toward `awm sensors baseline` when sensors are configured but no baseline exists', async () => {
|
|
334
334
|
// The team-rollout gap this addresses: a legacy repo adopts AWM, sensors get
|
|
335
335
|
// configured, and the ratchet mechanism exists to snapshot pre-existing debt —
|
|
336
336
|
// but nothing tells the operator it's there until they hit a wall of red
|
|
@@ -340,13 +340,13 @@ describe('preflight', () => {
|
|
|
340
340
|
bins: ['eslint'],
|
|
341
341
|
files: ['package.json'],
|
|
342
342
|
});
|
|
343
|
-
const report = (0, checks_1.preflight)(dir);
|
|
343
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
344
344
|
expect(check(report, 'sensors-baseline').ok).toBe(true);
|
|
345
345
|
expect(check(report, 'sensors-baseline').detail).toContain('no baseline yet');
|
|
346
346
|
expect(check(report, 'sensors-baseline').remedy).toContain('awm sensors baseline');
|
|
347
347
|
expect(report.status).toBe('ready');
|
|
348
348
|
});
|
|
349
|
-
it('reports the no-advisory-needed state when a baseline already exists, without nudging', () => {
|
|
349
|
+
it('reports the no-advisory-needed state when a baseline already exists, without nudging', async () => {
|
|
350
350
|
const dir = make({
|
|
351
351
|
manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .' } } },
|
|
352
352
|
bins: ['eslint'],
|
|
@@ -354,19 +354,19 @@ describe('preflight', () => {
|
|
|
354
354
|
});
|
|
355
355
|
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm'), { recursive: true });
|
|
356
356
|
fs_1.default.writeFileSync(path_1.default.join(dir, '.awm', 'sensors.baseline.json'), JSON.stringify({ lint: [] }));
|
|
357
|
-
const report = (0, checks_1.preflight)(dir);
|
|
357
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
358
358
|
expect(check(report, 'sensors-baseline').ok).toBe(true);
|
|
359
359
|
expect(check(report, 'sensors-baseline').detail).toBe('baseline present');
|
|
360
360
|
expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
|
|
361
361
|
expect(report.status).toBe('ready');
|
|
362
362
|
});
|
|
363
|
-
it('is omitted entirely when there is no manifest at all — nothing to baseline without sensors', () => {
|
|
363
|
+
it('is omitted entirely when there is no manifest at all — nothing to baseline without sensors', async () => {
|
|
364
364
|
const dir = make();
|
|
365
|
-
const report = (0, checks_1.preflight)(dir);
|
|
365
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
366
366
|
expect(report.checks.find(c => c.id === 'sensors-baseline')).toBeUndefined();
|
|
367
367
|
expect(report.status).toBe('not_configured');
|
|
368
368
|
});
|
|
369
|
-
it('does not nudge on a deliberate opt-out (every sensor disabled) — nothing to baseline', () => {
|
|
369
|
+
it('does not nudge on a deliberate opt-out (every sensor disabled) — nothing to baseline', async () => {
|
|
370
370
|
// Regression: the trigger condition originally checked only manifestExists, so a
|
|
371
371
|
// repo that deliberately opted out (checkManifest's own documented pattern: every
|
|
372
372
|
// sensor `enabled: false`) still got told to run `awm sensors baseline` — nothing
|
|
@@ -374,19 +374,19 @@ describe('preflight', () => {
|
|
|
374
374
|
const dir = make({
|
|
375
375
|
manifest: { pack: 'js-ts', sensors: { lint: { cmd: 'npx eslint .', enabled: false } } },
|
|
376
376
|
});
|
|
377
|
-
const report = (0, checks_1.preflight)(dir);
|
|
377
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
378
378
|
expect(check(report, 'sensors-baseline').ok).toBe(true);
|
|
379
379
|
expect(check(report, 'sensors-baseline').detail).toBe('no enabled sensors — nothing to baseline');
|
|
380
380
|
expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
|
|
381
381
|
});
|
|
382
|
-
it('does not nudge on an unparseable manifest — nothing to baseline', () => {
|
|
382
|
+
it('does not nudge on an unparseable manifest — nothing to baseline', async () => {
|
|
383
383
|
const dir = make({ manifest: '{not valid json' });
|
|
384
|
-
const report = (0, checks_1.preflight)(dir);
|
|
384
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
385
385
|
expect(check(report, 'sensors-baseline').ok).toBe(true);
|
|
386
386
|
expect(check(report, 'sensors-baseline').detail).toBe('no enabled sensors — nothing to baseline');
|
|
387
387
|
expect(check(report, 'sensors-baseline').remedy).toBeUndefined();
|
|
388
388
|
});
|
|
389
|
-
it('still nudges when the baseline path exists but is not a readable file (e.g. a stray directory)', () => {
|
|
389
|
+
it('still nudges when the baseline path exists but is not a readable file (e.g. a stray directory)', async () => {
|
|
390
390
|
// Regression: checking presence via `fs.existsSync` alone would have reported
|
|
391
391
|
// "baseline present" here, reassuring the operator that debt is suppressed —
|
|
392
392
|
// but the real gate (`readBaseline`, used by `partition()`) treats an unreadable
|
|
@@ -398,13 +398,13 @@ describe('preflight', () => {
|
|
|
398
398
|
files: ['package.json'],
|
|
399
399
|
});
|
|
400
400
|
fs_1.default.mkdirSync(path_1.default.join(dir, '.awm', 'sensors.baseline.json'), { recursive: true });
|
|
401
|
-
const report = (0, checks_1.preflight)(dir);
|
|
401
|
+
const report = await (0, checks_1.preflight)(dir);
|
|
402
402
|
expect(check(report, 'sensors-baseline').ok).toBe(true);
|
|
403
403
|
expect(check(report, 'sensors-baseline').detail).toContain('no baseline yet');
|
|
404
404
|
expect(check(report, 'sensors-baseline').remedy).toContain('awm sensors baseline');
|
|
405
405
|
});
|
|
406
406
|
});
|
|
407
|
-
it('tells the operator not to hand a broken harness to an unattended run', () => {
|
|
407
|
+
it('tells the operator not to hand a broken harness to an unattended run', async () => {
|
|
408
408
|
const out = (0, preflight_1.formatReport)({
|
|
409
409
|
status: 'not_configured',
|
|
410
410
|
checks: [{ id: 'manifest', ok: false, detail: 'no .awm/sensors.json', remedy: 'run `awm sensors init`' }],
|
|
@@ -412,7 +412,7 @@ describe('preflight', () => {
|
|
|
412
412
|
expect(out).toContain('unattended');
|
|
413
413
|
expect(out).toContain('awm sensors init');
|
|
414
414
|
});
|
|
415
|
-
it('pads the id column to the widest id actually present, not a hardcoded width', () => {
|
|
415
|
+
it('pads the id column to the widest id actually present, not a hardcoded width', async () => {
|
|
416
416
|
// Regression: a literal `.padEnd(9)` silently misaligned once `sensors-baseline`
|
|
417
417
|
// (16 chars) was added as a check id — every detail column shifted left of where
|
|
418
418
|
// shorter ids' details landed. The width must be derived from the report itself.
|
|
@@ -12,9 +12,24 @@ const GIT = (cwd, cmd) => (0, child_process_1.execSync)(`git -c user.email=t@t.t
|
|
|
12
12
|
function makeSourceRepo(base, opts) {
|
|
13
13
|
const dir = path_1.default.join(base, `src-${opts.skill ?? 'empty'}`);
|
|
14
14
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
15
|
-
if (
|
|
15
|
+
if (opts.contentSymlink) {
|
|
16
|
+
fs_1.default.symlinkSync(opts.contentSymlink, path_1.default.join(dir, 'skills'));
|
|
17
|
+
}
|
|
18
|
+
else if (opts.contentFile) {
|
|
19
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, opts.contentFile), 'not a content directory');
|
|
20
|
+
}
|
|
21
|
+
else if (opts.nestedHookSymlink) {
|
|
22
|
+
fs_1.default.mkdirSync(path_1.default.join(dir, 'hooks'), { recursive: true });
|
|
23
|
+
fs_1.default.symlinkSync(opts.nestedHookSymlink, path_1.default.join(dir, 'hooks', 'session-start'));
|
|
24
|
+
}
|
|
25
|
+
else if (!opts.empty && opts.skill) {
|
|
16
26
|
fs_1.default.mkdirSync(path_1.default.join(dir, 'skills', opts.skill), { recursive: true });
|
|
17
|
-
|
|
27
|
+
if (opts.nestedSkillSymlink) {
|
|
28
|
+
fs_1.default.symlinkSync(opts.nestedSkillSymlink, path_1.default.join(dir, 'skills', opts.skill, 'SKILL.md'));
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
fs_1.default.writeFileSync(path_1.default.join(dir, 'skills', opts.skill, 'SKILL.md'), `---\nname: ${opts.skill}\ndescription: d\n---\n`);
|
|
32
|
+
}
|
|
18
33
|
}
|
|
19
34
|
else {
|
|
20
35
|
fs_1.default.writeFileSync(path_1.default.join(dir, 'README.md'), 'no content dirs');
|
|
@@ -97,6 +112,48 @@ describe('addRegistry', () => {
|
|
|
97
112
|
expect(readRegistriesConfig()).toEqual([]);
|
|
98
113
|
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/bad'))).toBe(false);
|
|
99
114
|
});
|
|
115
|
+
it('rejects a regular file that masquerades as a content directory', async () => {
|
|
116
|
+
const source = makeSourceRepo(tmpWork, { contentFile: 'skills' });
|
|
117
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
118
|
+
const result = await addRegistry(source, 'not-a-registry');
|
|
119
|
+
expect(result.ok).toBe(false);
|
|
120
|
+
expect(result.error).toMatch(/skills\/, bundles\/, workflows\/, agents\//);
|
|
121
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
122
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
123
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/not-a-registry'))).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('rejects a symlink that masquerades as a content directory', async () => {
|
|
126
|
+
const source = makeSourceRepo(tmpWork, { contentSymlink: '.' });
|
|
127
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
128
|
+
const result = await addRegistry(source, 'linked-content');
|
|
129
|
+
expect(result.ok).toBe(false);
|
|
130
|
+
expect(result.error).toMatch(/skills\/, bundles\/, workflows\/, agents\//);
|
|
131
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
132
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
133
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/linked-content'))).toBe(false);
|
|
134
|
+
});
|
|
135
|
+
it('rejects a nested artifact symlink before discovery can read it', async () => {
|
|
136
|
+
const outside = path_1.default.join(tmpWork, 'outside-skill.md');
|
|
137
|
+
fs_1.default.writeFileSync(outside, 'host-only content');
|
|
138
|
+
const source = makeSourceRepo(tmpWork, { skill: 'linked', nestedSkillSymlink: outside });
|
|
139
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
140
|
+
const result = await addRegistry(source, 'nested-link');
|
|
141
|
+
expect(result.ok).toBe(false);
|
|
142
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
143
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
144
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/nested-link'))).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
it('rejects a nested hook symlink before capability consumers can use it', async () => {
|
|
147
|
+
const outside = path_1.default.join(tmpWork, 'outside-hook');
|
|
148
|
+
fs_1.default.writeFileSync(outside, 'host-only content');
|
|
149
|
+
const source = makeSourceRepo(tmpWork, { nestedHookSymlink: outside });
|
|
150
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
151
|
+
const result = await addRegistry(source, 'nested-hook-link');
|
|
152
|
+
expect(result.ok).toBe(false);
|
|
153
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
154
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
155
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries/nested-hook-link'))).toBe(false);
|
|
156
|
+
});
|
|
100
157
|
it('is atomic: artifact collision with existing configured registry → no config, cleanup, error names both', async () => {
|
|
101
158
|
// First registry already registered with the 'alpha' skill
|
|
102
159
|
const source1 = path_1.default.join(tmpWork, 'src-alpha-1');
|
|
@@ -133,6 +190,16 @@ describe('addRegistry', () => {
|
|
|
133
190
|
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
134
191
|
expect(readRegistriesConfig()).toEqual([]);
|
|
135
192
|
});
|
|
193
|
+
it.each(['', '..', 'bad..name', 'a/b', 'a\\b'])('rejects unsafe registry name %s without cloning or writing config', async (name) => {
|
|
194
|
+
const source = makeSourceRepo(tmpWork, { skill: 'alpha' });
|
|
195
|
+
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
196
|
+
const result = await addRegistry(source, name);
|
|
197
|
+
expect(result.ok).toBe(false);
|
|
198
|
+
expect(result.error).toMatch(/Invalid registry name/);
|
|
199
|
+
const { readRegistriesConfig } = require('../../../src/core/registries');
|
|
200
|
+
expect(readRegistriesConfig()).toEqual([]);
|
|
201
|
+
expect(fs_1.default.existsSync(path_1.default.join(tmpHome, '.awm/registries', name))).toBe(false);
|
|
202
|
+
});
|
|
136
203
|
it('rejects duplicate registry name and clone failure without writing config', async () => {
|
|
137
204
|
const source = makeSourceRepo(tmpWork, { skill: 'alpha' });
|
|
138
205
|
const { addRegistry } = require('../../../src/commands/registry/add');
|
|
@@ -40,4 +40,18 @@ describe('registry override status', () => {
|
|
|
40
40
|
const { overrideStatus } = require('../../../src/commands/registry/status');
|
|
41
41
|
expect(overrideStatus(team, [])).toEqual([]);
|
|
42
42
|
});
|
|
43
|
+
it('registry list delegates discovery eligibility to the validated-roots guard', () => {
|
|
44
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/commands/registry/index.ts'), 'utf8');
|
|
45
|
+
expect(source).toContain('const safeRoots = new Set(contentRoots());');
|
|
46
|
+
expect(source).toContain('if (!canDiscoverRegistryContent(r.contentRoot, safeRoots))');
|
|
47
|
+
});
|
|
48
|
+
it('prevents discovery when a registry root was excluded from validated roots', () => {
|
|
49
|
+
const { canDiscoverRegistryContent } = require('../../../src/commands/registry/status');
|
|
50
|
+
expect(canDiscoverRegistryContent('/unsafe/registry', new Set())).toBe(false);
|
|
51
|
+
expect(canDiscoverRegistryContent('/safe/registry', new Set(['/safe/registry']))).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
it('registry list gives a recoverable instruction for an unsafe existing registry', () => {
|
|
54
|
+
const source = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../../../src/commands/registry/index.ts'), 'utf8');
|
|
55
|
+
expect(source).toContain("unsafe layout — remove the registry and add it again");
|
|
56
|
+
});
|
|
43
57
|
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const contract_1 = require("../../../../src/commands/sensors/compatibility/contract");
|
|
4
|
+
const coverage = {
|
|
5
|
+
schemaVersion: 1,
|
|
6
|
+
classes: {
|
|
7
|
+
formatting: {
|
|
8
|
+
description: 'Formatting is checked.',
|
|
9
|
+
detectors: [{ sensor: 'lint' }],
|
|
10
|
+
remedy: { summary: 'Add a formatter.', command: 'npm install prettier' },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
function validPack() {
|
|
15
|
+
return {
|
|
16
|
+
schemaVersion: 2,
|
|
17
|
+
name: 'js-ts', description: 'fixture', detects: ['package.json'],
|
|
18
|
+
sensors: {
|
|
19
|
+
lint: {
|
|
20
|
+
applicability: { allFiles: ['package.json'] },
|
|
21
|
+
variants: [{
|
|
22
|
+
id: 'eslint-9',
|
|
23
|
+
priority: 10,
|
|
24
|
+
requirements: { tool: 'eslint', toolRange: '>=9.0.0 <10.0.0', runtime: 'node', runtimeRange: '>=20.0.0' },
|
|
25
|
+
certifiedRange: '>=9.0.0 <10.0.0',
|
|
26
|
+
assets: ['eslint.config.awm.mjs'], formatter: 'eslint-llm', probe: { kind: 'eslint-print-config' },
|
|
27
|
+
command: {
|
|
28
|
+
executable: 'eslint',
|
|
29
|
+
resolution: 'node-modules-bin',
|
|
30
|
+
args: ['--format', 'json', '{files}'],
|
|
31
|
+
fileInput: { placeholder: '{files}', extensions: ['.ts', '.tsx'] },
|
|
32
|
+
},
|
|
33
|
+
}],
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
coverage,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
describe('sensor pack v2 contract', () => {
|
|
40
|
+
it('parses a valid versioned pack', () => {
|
|
41
|
+
expect((0, contract_1.parseSensorPack)(validPack(), 'pack.json')).toMatchObject({ kind: 'v2', pack: validPack() });
|
|
42
|
+
});
|
|
43
|
+
it('keeps an unversioned pack on the legacy compatibility path', () => {
|
|
44
|
+
const legacy = { name: 'legacy', sensors: {} };
|
|
45
|
+
expect((0, contract_1.parseSensorPack)(legacy, 'pack.json')).toMatchObject({ kind: 'legacy', pack: { ...legacy, compatibility: { state: 'compatible-unverified' } } });
|
|
46
|
+
});
|
|
47
|
+
test.each([
|
|
48
|
+
[{ name: 'legacy', detects: [null], sensors: {} }, 'detects[0]'],
|
|
49
|
+
[{ name: 'legacy', detects: ['package.json'], sensors: { lint: { unknown: true } } }, 'unknown field'],
|
|
50
|
+
[{ name: 'legacy', detects: ['package.json'], sensors: { lint: { defaultCmd: 'eslint\u0000 .' } } }, 'defaultCmd'],
|
|
51
|
+
[{ name: 'legacy', detects: ['package.json'], sensors: { lint: { changedExtensions: ['.ts', 3] } } }, 'changedExtensions[1]'],
|
|
52
|
+
[{ name: 'legacy', detects: ['package.json'], sensors: { 'bad/name': {} } }, 'sensor id'],
|
|
53
|
+
])('rejects recursively malformed legacy content %j', (input, message) => {
|
|
54
|
+
expect(() => (0, contract_1.parseSensorPack)(input, 'legacy-pack.json')).toThrow(message);
|
|
55
|
+
});
|
|
56
|
+
it('preserves legacy config file fallbacks', () => {
|
|
57
|
+
expect((0, contract_1.parseSensorPack)({ name: 'legacy', sensors: { lint: { configFile: 'eslint.config.js', configFileFallback: '.eslintrc.json' } } }, 'legacy')).toMatchObject({ kind: 'legacy', pack: { sensors: { lint: { configFile: 'eslint.config.js', configFileFallback: '.eslintrc.json' } } } });
|
|
58
|
+
});
|
|
59
|
+
it('rejects malformed applicability and probe extensions', () => {
|
|
60
|
+
const variant = validPack().sensors.lint.variants[0];
|
|
61
|
+
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { applicability: { allFiles: [3] }, variants: [variant] } } }, 'pack')).toThrow('allFiles[0]');
|
|
62
|
+
expect(() => (0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...variant, probe: { kind: 'version', extra: true } }] } } }, 'pack')).toThrow('unknown field');
|
|
63
|
+
});
|
|
64
|
+
it('preserves configFiles and rejects incomplete public overlap input', () => {
|
|
65
|
+
const variant = { ...validPack().sensors.lint.variants[0], requirements: { ...validPack().sensors.lint.variants[0].requirements, configFiles: ['eslint.config.js'] } };
|
|
66
|
+
expect((0, contract_1.parseSensorPack)({ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [variant] } } }, 'pack')).toMatchObject({ kind: 'v2', pack: { sensors: { lint: { variants: [{ requirements: { configFiles: ['eslint.config.js'] } }] } } } });
|
|
67
|
+
const complete = validPack().sensors.lint.variants[0];
|
|
68
|
+
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([{ ...complete, id: '../bad' }])).toThrow('stable lowercase id');
|
|
69
|
+
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([{ ...complete, command: undefined }])).toThrow('command');
|
|
70
|
+
});
|
|
71
|
+
it('rejects equal-priority variants with overlapping operational tool ranges', () => {
|
|
72
|
+
const first = validPack().sensors.lint.variants[0];
|
|
73
|
+
const second = { ...first, id: 'eslint-9-alt', certifiedRange: '>=10 <11', requirements: { ...first.requirements, toolRange: '>=9.5 <10.5' } };
|
|
74
|
+
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([first, second])).toThrow('overlap');
|
|
75
|
+
});
|
|
76
|
+
it('accepts overlapping tool ranges when runtime ranges are disjoint', () => {
|
|
77
|
+
const first = validPack().sensors.lint.variants[0];
|
|
78
|
+
const second = { ...first, id: 'eslint-9-node-old', requirements: { ...first.requirements, toolRange: '>=9.5 <10.5', runtimeRange: '>=18 <20' } };
|
|
79
|
+
expect(() => (0, contract_1.assertNoEqualPriorityOverlap)([first, second])).not.toThrow();
|
|
80
|
+
});
|
|
81
|
+
test.each([
|
|
82
|
+
[{ ...validPack(), schemaVersion: 3 }, 'supported: legacy, 2'],
|
|
83
|
+
[{ ...validPack(), sensors: { lint: { variants: [] } } }, 'variants'],
|
|
84
|
+
[{ ...validPack(), name: '../escape' }, 'name'],
|
|
85
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], assets: ['../secret'] }] } } }, 'asset'],
|
|
86
|
+
[{ ...validPack(), sensors: { lint: { variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'sh', resolution: 'path', args: ['-c', 'echo nope'] } }] } } }, 'executable'],
|
|
87
|
+
[{ ...validPack(), coverage: { ...coverage, schemaVersion: 2 } }, 'coverage.schemaVersion'],
|
|
88
|
+
[{ ...validPack(), sensors: { lint: { variants: [{ ...validPack().sensors.lint.variants[0], certifiedRange: 'not-a-range' }] } } }, 'certifiedRange'],
|
|
89
|
+
[{ ...validPack(), sensors: { lint: validPack().sensors.lint, lint2: validPack().sensors.lint } }, 'variant id'],
|
|
90
|
+
[{ ...validPack(), sensors: { lint: { variants: [validPack().sensors.lint.variants[0], { ...validPack().sensors.lint.variants[0], id: 'eslint-9-next', certifiedRange: '>=9.1.0 <10.0.0' }] } } }, 'overlap'],
|
|
91
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], assets: ['C:/secret'] }] } } }, 'asset'],
|
|
92
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { executable: 'cmd.exe', resolution: 'path', args: ['x'] } }] } } }, 'executable'],
|
|
93
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], command: { ...validPack().sensors.lint.variants[0].command, args: ['prefix{files}'] } }] } } }, 'embed'],
|
|
94
|
+
[{ ...validPack(), sensors: { lint: { ...validPack().sensors.lint, variants: [{ ...validPack().sensors.lint.variants[0], formatter: 'bad\nformat' }] } } }, 'formatter'],
|
|
95
|
+
])('rejects malformed pack %j', (input, message) => {
|
|
96
|
+
expect(() => (0, contract_1.parseSensorPack)(input, 'pack.json')).toThrow(message);
|
|
97
|
+
});
|
|
98
|
+
});
|