@sabaiway/agent-workflow-kit 1.15.0 → 1.15.2
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/CHANGELOG.md +43 -0
- package/SKILL.md +3 -2
- package/capability.json +1 -1
- package/package.json +5 -2
- package/references/contracts.md +2 -0
- package/bin/install.test.mjs +0 -347
- package/tools/delegation.test.mjs +0 -116
- package/tools/detect-backends.test.mjs +0 -444
- package/tools/engine-source.test.mjs +0 -280
- package/tools/family-registry.test.mjs +0 -279
- package/tools/fs-safe.test.mjs +0 -346
- package/tools/hide-footprint.integration.test.mjs +0 -168
- package/tools/hide-footprint.test.mjs +0 -463
- package/tools/inject-methodology.test.mjs +0 -689
- package/tools/known-footprint.test.mjs +0 -271
- package/tools/manifest/fixtures/bad-available/SKILL.md +0 -7
- package/tools/manifest/fixtures/bad-available/capability.json +0 -10
- package/tools/manifest/fixtures/detect-array/SKILL.md +0 -7
- package/tools/manifest/fixtures/detect-array/capability.json +0 -10
- package/tools/manifest/fixtures/malformed-json/capability.json +0 -1
- package/tools/manifest/fixtures/metadata-version/SKILL.md +0 -10
- package/tools/manifest/fixtures/metadata-version/capability.json +0 -9
- package/tools/manifest/fixtures/missing-key/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-key/capability.json +0 -8
- package/tools/manifest/fixtures/missing-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/missing-source/capability.json +0 -11
- package/tools/manifest/fixtures/nested-version-decoy/SKILL.md +0 -10
- package/tools/manifest/fixtures/nested-version-decoy/capability.json +0 -9
- package/tools/manifest/fixtures/null-root/capability.json +0 -1
- package/tools/manifest/fixtures/provides-roles-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/provides-roles-mismatch/bin/run.sh +0 -2
- package/tools/manifest/fixtures/provides-roles-mismatch/capability.json +0 -11
- package/tools/manifest/fixtures/stub/capability.json +0 -10
- package/tools/manifest/fixtures/traversal-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/traversal-source/capability.json +0 -11
- package/tools/manifest/fixtures/unknown-schema/capability.json +0 -9
- package/tools/manifest/fixtures/valid/SKILL.md +0 -10
- package/tools/manifest/fixtures/valid/bin/run.sh +0 -3
- package/tools/manifest/fixtures/valid/capability.json +0 -18
- package/tools/manifest/fixtures/version-mismatch/SKILL.md +0 -7
- package/tools/manifest/fixtures/version-mismatch/capability.json +0 -9
- package/tools/manifest/fixtures/win-absolute-source/SKILL.md +0 -7
- package/tools/manifest/fixtures/win-absolute-source/capability.json +0 -11
- package/tools/manifest/validate.test.mjs +0 -73
- package/tools/procedures.test.mjs +0 -303
- package/tools/recipes.test.mjs +0 -538
- package/tools/release-scan.test.mjs +0 -41
- package/tools/setup-backends.test.mjs +0 -500
- package/tools/uninstall.integration.test.mjs +0 -144
- package/tools/uninstall.test.mjs +0 -401
- package/tools/velocity-profile.test.mjs +0 -496
|
@@ -1,280 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { join } from 'node:path';
|
|
4
|
-
import {
|
|
5
|
-
resolveEngineDir,
|
|
6
|
-
detectEngine,
|
|
7
|
-
readEngineFragment,
|
|
8
|
-
ENGINE_ENV,
|
|
9
|
-
EXPECTED_ENGINE_NAME,
|
|
10
|
-
ENGINE_FRAGMENT_REL,
|
|
11
|
-
ORCHESTRATION_FRAGMENT_REL,
|
|
12
|
-
PROCEDURES_FRAGMENT_REL,
|
|
13
|
-
} from './engine-source.mjs';
|
|
14
|
-
|
|
15
|
-
// A valid engine dir = it exists (dir), the fragment file exists, and the validator reports a
|
|
16
|
-
// VALID methodology-engine with the right name. The deps below let every branch be exercised
|
|
17
|
-
// in-process without touching the real filesystem.
|
|
18
|
-
const ENGINE_DIR = '/home/u/.claude/skills/agent-workflow-engine';
|
|
19
|
-
|
|
20
|
-
// statType stub: ENGINE_DIR → 'dir', the fragment → 'file', everything else → null.
|
|
21
|
-
const okStatType = (path) =>
|
|
22
|
-
path === ENGINE_DIR ? 'dir' : path === join(ENGINE_DIR, ENGINE_FRAGMENT_REL) ? 'file' : null;
|
|
23
|
-
|
|
24
|
-
const okReport = {
|
|
25
|
-
result: 'valid',
|
|
26
|
-
kind: 'methodology-engine',
|
|
27
|
-
name: EXPECTED_ENGINE_NAME,
|
|
28
|
-
available: true,
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const deps = (overrides = {}) => ({
|
|
32
|
-
validate: () => okReport,
|
|
33
|
-
statType: okStatType,
|
|
34
|
-
...overrides,
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('resolveEngineDir — env override vs the ~/.claude default', () => {
|
|
38
|
-
it('uses AGENT_WORKFLOW_ENGINE_DIR when set, tagging source=env', () => {
|
|
39
|
-
const out = resolveEngineDir({ env: { [ENGINE_ENV]: '/custom/engine' }, home: '/home/u' });
|
|
40
|
-
assert.deepEqual(out, { dir: '/custom/engine', source: 'env' });
|
|
41
|
-
});
|
|
42
|
-
it('falls back to ~/.claude/skills/agent-workflow-engine, tagging source=default', () => {
|
|
43
|
-
const out = resolveEngineDir({ env: {}, home: '/home/u' });
|
|
44
|
-
assert.equal(out.source, 'default');
|
|
45
|
-
assert.equal(out.dir, join('/home/u', '.claude/skills/agent-workflow-engine'));
|
|
46
|
-
});
|
|
47
|
-
it('treats an empty env value as unset (source=default)', () => {
|
|
48
|
-
const out = resolveEngineDir({ env: { [ENGINE_ENV]: '' }, home: '/home/u' });
|
|
49
|
-
assert.equal(out.source, 'default');
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
describe('detectEngine — happy path + each distinct failure reason', () => {
|
|
54
|
-
it('ok when dir exists, manifest VALID methodology-engine, right name, fragment present', () => {
|
|
55
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps());
|
|
56
|
-
assert.equal(out.ok, true);
|
|
57
|
-
assert.equal(out.dir, ENGINE_DIR);
|
|
58
|
-
assert.match(out.reason, /methodology-engine/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('env-set-but-missing: dir absent AND source=env → distinct reason', () => {
|
|
62
|
-
const out = detectEngine(ENGINE_DIR, { source: 'env' }, deps({ statType: () => null }));
|
|
63
|
-
assert.equal(out.ok, false);
|
|
64
|
-
assert.match(out.reason, /env-set-but-missing/);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('not-installed: dir absent AND source=default → not an env-set reason', () => {
|
|
68
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ statType: () => null }));
|
|
69
|
-
assert.equal(out.ok, false);
|
|
70
|
-
assert.match(out.reason, /not installed/i);
|
|
71
|
-
assert.doesNotMatch(out.reason, /env-set-but-missing/);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('invalid manifest → reason names the validator result', () => {
|
|
75
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ validate: () => ({ ...okReport, result: 'invalid' }) }));
|
|
76
|
-
assert.equal(out.ok, false);
|
|
77
|
-
assert.match(out.reason, /invalid/);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('wrong kind → reason names the kind', () => {
|
|
81
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ validate: () => ({ ...okReport, kind: 'memory-substrate' }) }));
|
|
82
|
-
assert.equal(out.ok, false);
|
|
83
|
-
assert.match(out.reason, /kind/);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('wrong name → reason names the name mismatch', () => {
|
|
87
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ validate: () => ({ ...okReport, name: 'something-else' }) }));
|
|
88
|
-
assert.equal(out.ok, false);
|
|
89
|
-
assert.match(out.reason, /name/);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('declared stub (available:false) → reason names the stub', () => {
|
|
93
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ validate: () => ({ ...okReport, available: false }) }));
|
|
94
|
-
assert.equal(out.ok, false);
|
|
95
|
-
assert.match(out.reason, /stub|available:false/);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('missing fragment → reason names the fragment path', () => {
|
|
99
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ statType: (p) => (p === ENGINE_DIR ? 'dir' : null) }));
|
|
100
|
-
assert.equal(out.ok, false);
|
|
101
|
-
assert.match(out.reason, /fragment/);
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('a validator that THROWS (corrupt engine, e.g. EISDIR) → ok:false, no raw error escapes', () => {
|
|
105
|
-
const out = detectEngine(
|
|
106
|
-
ENGINE_DIR,
|
|
107
|
-
{ source: 'default' },
|
|
108
|
-
deps({
|
|
109
|
-
validate: () => {
|
|
110
|
-
throw new Error('EISDIR: illegal operation on a directory, read');
|
|
111
|
-
},
|
|
112
|
-
}),
|
|
113
|
-
);
|
|
114
|
-
assert.equal(out.ok, false);
|
|
115
|
-
assert.match(out.reason, /invalid/);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('readEngineFragment — live read or loud throw', () => {
|
|
120
|
-
it('returns the fragment bytes on the happy path', () => {
|
|
121
|
-
const out = readEngineFragment(ENGINE_DIR, deps({ source: 'default', readFileSync: () => 'FRAGMENT BODY' }));
|
|
122
|
-
assert.equal(out, 'FRAGMENT BODY');
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('throws naming the dir + the exact install command when the engine is absent', () => {
|
|
126
|
-
assert.throws(
|
|
127
|
-
() => readEngineFragment(ENGINE_DIR, deps({ source: 'env', statType: () => null })),
|
|
128
|
-
(err) => {
|
|
129
|
-
assert.match(err.message, /methodology engine not found\/invalid/);
|
|
130
|
-
assert.match(err.message, new RegExp(ENGINE_DIR.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
|
|
131
|
-
assert.match(err.message, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
132
|
-
assert.match(err.message, new RegExp(ENGINE_ENV));
|
|
133
|
-
return true;
|
|
134
|
-
},
|
|
135
|
-
);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('throws when the manifest is invalid (never falls back)', () => {
|
|
139
|
-
assert.throws(
|
|
140
|
-
() => readEngineFragment(ENGINE_DIR, deps({ source: 'default', validate: () => ({ ...okReport, result: 'invalid' }) })),
|
|
141
|
-
/methodology engine not found\/invalid/,
|
|
142
|
-
);
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('a THROWING validator still yields the stable install message (no raw fs error escapes)', () => {
|
|
146
|
-
assert.throws(
|
|
147
|
-
() =>
|
|
148
|
-
readEngineFragment(
|
|
149
|
-
ENGINE_DIR,
|
|
150
|
-
deps({
|
|
151
|
-
source: 'default',
|
|
152
|
-
validate: () => {
|
|
153
|
-
throw new Error('EISDIR: illegal operation on a directory, read');
|
|
154
|
-
},
|
|
155
|
-
}),
|
|
156
|
-
),
|
|
157
|
-
(err) => {
|
|
158
|
-
assert.match(err.message, /methodology engine not found\/invalid/);
|
|
159
|
-
assert.match(err.message, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
160
|
-
return true;
|
|
161
|
-
},
|
|
162
|
-
);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('throws with the install command when the fragment file is unreadable', () => {
|
|
166
|
-
assert.throws(
|
|
167
|
-
() =>
|
|
168
|
-
readEngineFragment(
|
|
169
|
-
ENGINE_DIR,
|
|
170
|
-
deps({
|
|
171
|
-
source: 'default',
|
|
172
|
-
readFileSync: () => {
|
|
173
|
-
throw new Error('EISDIR');
|
|
174
|
-
},
|
|
175
|
-
}),
|
|
176
|
-
),
|
|
177
|
-
(err) => {
|
|
178
|
-
assert.match(err.message, /fragment unreadable: EISDIR/);
|
|
179
|
-
assert.match(err.message, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
180
|
-
return true;
|
|
181
|
-
},
|
|
182
|
-
);
|
|
183
|
-
});
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
// The orchestration fragment is a SECOND bounded fragment (Plan 4), selected via deps.rel /
|
|
187
|
-
// detectEngine({ rel }) — non-breaking: the default rel stays the methodology fragment so every
|
|
188
|
-
// existing methodology call site is unchanged.
|
|
189
|
-
describe('detectEngine / readEngineFragment — orchestration fragment via rel', () => {
|
|
190
|
-
// statType that knows both fragments live in the engine (a current >=1.2.0 engine).
|
|
191
|
-
const bothPresent = (path) =>
|
|
192
|
-
path === ENGINE_DIR
|
|
193
|
-
? 'dir'
|
|
194
|
-
: path === join(ENGINE_DIR, ENGINE_FRAGMENT_REL) || path === join(ENGINE_DIR, ORCHESTRATION_FRAGMENT_REL)
|
|
195
|
-
? 'file'
|
|
196
|
-
: null;
|
|
197
|
-
|
|
198
|
-
it('verifies the orchestration fragment when rel is the orchestration path', () => {
|
|
199
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default', rel: ORCHESTRATION_FRAGMENT_REL }, deps({ statType: bothPresent }));
|
|
200
|
-
assert.equal(out.ok, true);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('an older engine (no orchestration fragment) → detect not-ok, the reason names that fragment', () => {
|
|
204
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default', rel: ORCHESTRATION_FRAGMENT_REL }, deps()); // okStatType: only the methodology fragment is a file
|
|
205
|
-
assert.equal(out.ok, false);
|
|
206
|
-
assert.match(out.reason, /orchestration-slot\.md/);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('the methodology read is unaffected by the new rel default (back-compat)', () => {
|
|
210
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default' }, deps({ statType: bothPresent }));
|
|
211
|
-
assert.equal(out.ok, true);
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it('readEngineFragment reads the orchestration fragment bytes when deps.rel is set', () => {
|
|
215
|
-
const out = readEngineFragment(
|
|
216
|
-
ENGINE_DIR,
|
|
217
|
-
deps({ source: 'default', rel: ORCHESTRATION_FRAGMENT_REL, statType: bothPresent, readFileSync: () => 'ORCH BODY' }),
|
|
218
|
-
);
|
|
219
|
-
assert.equal(out, 'ORCH BODY');
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
it('readEngineFragment STOPs loudly when the orchestration fragment is absent (older engine)', () => {
|
|
223
|
-
assert.throws(
|
|
224
|
-
() => readEngineFragment(ENGINE_DIR, deps({ source: 'default', rel: ORCHESTRATION_FRAGMENT_REL })),
|
|
225
|
-
(err) => {
|
|
226
|
-
assert.match(err.message, /methodology engine not found\/invalid/);
|
|
227
|
-
assert.match(err.message, /orchestration-slot\.md/);
|
|
228
|
-
assert.match(err.message, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
229
|
-
return true;
|
|
230
|
-
},
|
|
231
|
-
);
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
// The activity-procedures canon is a THIRD live-read fragment (engine >= 1.3.0), selected the same way
|
|
236
|
-
// (deps.rel / detectEngine({ rel })). An engine too old to ship it must read as not-ok naming the
|
|
237
|
-
// fragment, and readEngineFragment must STOP loudly — the procedures CLI maps that to a clean exit 1.
|
|
238
|
-
describe('detectEngine / readEngineFragment — procedures fragment via rel', () => {
|
|
239
|
-
const proceduresPresent = (path) =>
|
|
240
|
-
path === ENGINE_DIR
|
|
241
|
-
? 'dir'
|
|
242
|
-
: path === join(ENGINE_DIR, ENGINE_FRAGMENT_REL) || path === join(ENGINE_DIR, PROCEDURES_FRAGMENT_REL)
|
|
243
|
-
? 'file'
|
|
244
|
-
: null;
|
|
245
|
-
|
|
246
|
-
it('exposes the procedures fragment rel constant', () => {
|
|
247
|
-
assert.equal(PROCEDURES_FRAGMENT_REL, 'references/procedures.md');
|
|
248
|
-
});
|
|
249
|
-
|
|
250
|
-
it('verifies the procedures fragment when rel is the procedures path', () => {
|
|
251
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default', rel: PROCEDURES_FRAGMENT_REL }, deps({ statType: proceduresPresent }));
|
|
252
|
-
assert.equal(out.ok, true);
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
it('an older engine (no procedures fragment) → detect not-ok, the reason names that fragment', () => {
|
|
256
|
-
const out = detectEngine(ENGINE_DIR, { source: 'default', rel: PROCEDURES_FRAGMENT_REL }, deps()); // okStatType: only the methodology fragment is a file
|
|
257
|
-
assert.equal(out.ok, false);
|
|
258
|
-
assert.match(out.reason, /procedures\.md/);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('readEngineFragment reads the procedures fragment bytes when deps.rel is set', () => {
|
|
262
|
-
const out = readEngineFragment(
|
|
263
|
-
ENGINE_DIR,
|
|
264
|
-
deps({ source: 'default', rel: PROCEDURES_FRAGMENT_REL, statType: proceduresPresent, readFileSync: () => 'PROCEDURES BODY' }),
|
|
265
|
-
);
|
|
266
|
-
assert.equal(out, 'PROCEDURES BODY');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('readEngineFragment STOPs loudly when the procedures fragment is absent (engine < 1.3.0)', () => {
|
|
270
|
-
assert.throws(
|
|
271
|
-
() => readEngineFragment(ENGINE_DIR, deps({ source: 'default', rel: PROCEDURES_FRAGMENT_REL })),
|
|
272
|
-
(err) => {
|
|
273
|
-
assert.match(err.message, /methodology engine not found\/invalid/);
|
|
274
|
-
assert.match(err.message, /procedures\.md/);
|
|
275
|
-
assert.match(err.message, /npx @sabaiway\/agent-workflow-engine@latest init/);
|
|
276
|
-
return true;
|
|
277
|
-
},
|
|
278
|
-
);
|
|
279
|
-
});
|
|
280
|
-
});
|
|
@@ -1,279 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { readFileSync } from 'node:fs';
|
|
4
|
-
import { resolve, dirname, join } from 'node:path';
|
|
5
|
-
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import {
|
|
7
|
-
FAMILY_MEMBERS,
|
|
8
|
-
classifyMember,
|
|
9
|
-
surveyFamily,
|
|
10
|
-
surveyProject,
|
|
11
|
-
NOT_INSTALLED,
|
|
12
|
-
UNSUPPORTED_SCHEMA,
|
|
13
|
-
INVALID_MANIFEST,
|
|
14
|
-
STUB,
|
|
15
|
-
FOREIGN,
|
|
16
|
-
OK,
|
|
17
|
-
UNKNOWN,
|
|
18
|
-
} from './family-registry.mjs';
|
|
19
|
-
import { VALID, INVALID, UNSUPPORTED } from './manifest/validate.mjs';
|
|
20
|
-
import { START_MARKER } from './hide-footprint.mjs';
|
|
21
|
-
import { ORCHESTRATION_FRAGMENT_REL, PROCEDURES_FRAGMENT_REL } from './engine-source.mjs';
|
|
22
|
-
|
|
23
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
24
|
-
const REPO_ROOT = resolve(__dirname, '../..'); // agent-workflow-kit/tools → repo root
|
|
25
|
-
|
|
26
|
-
// Build classifyMember deps that present the marker, with an injectable validate/readVersion.
|
|
27
|
-
const installedDeps = ({ report, version = '9.9.9', home = '/home/test' }) => ({
|
|
28
|
-
exists: () => true,
|
|
29
|
-
stat: () => ({ isFile: () => true }),
|
|
30
|
-
getenv: {},
|
|
31
|
-
home,
|
|
32
|
-
validate: () => report,
|
|
33
|
-
readVersion: () => ({ version }),
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
const KIT = FAMILY_MEMBERS.find((m) => m.name === 'agent-workflow-kit');
|
|
37
|
-
|
|
38
|
-
// ── classifyMember ───────────────────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
describe('classifyMember', () => {
|
|
41
|
-
it('marks an absent marker as not-installed (no skillDir, no version)', () => {
|
|
42
|
-
const r = classifyMember(KIT, { exists: () => false, getenv: {}, home: '/home/test' });
|
|
43
|
-
assert.equal(r.manifestState, NOT_INSTALLED);
|
|
44
|
-
assert.equal(r.installed, false);
|
|
45
|
-
assert.equal(r.skillDir, null);
|
|
46
|
-
assert.equal(r.version, null);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('classifies a valid matching manifest as ok and reports the authoritative version', () => {
|
|
50
|
-
const r = classifyMember(KIT, installedDeps({
|
|
51
|
-
report: { result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: true },
|
|
52
|
-
version: '1.12.0',
|
|
53
|
-
}));
|
|
54
|
-
assert.equal(r.manifestState, OK);
|
|
55
|
-
assert.equal(r.installed, true);
|
|
56
|
-
assert.equal(r.version, '1.12.0');
|
|
57
|
-
assert.match(r.skillDir, /\.claude\/skills\/agent-workflow-kit$/);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('classifies a wrong name/kind as foreign (never ours, never removed by uninstall)', () => {
|
|
61
|
-
const r = classifyMember(KIT, installedDeps({
|
|
62
|
-
report: { result: VALID, name: 'something-else', kind: 'composition-root', available: true },
|
|
63
|
-
}));
|
|
64
|
-
assert.equal(r.manifestState, FOREIGN);
|
|
65
|
-
assert.equal(r.version, null); // version only for ok
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('classifies available:false as a stub', () => {
|
|
69
|
-
const r = classifyMember(KIT, installedDeps({
|
|
70
|
-
report: { result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: false },
|
|
71
|
-
}));
|
|
72
|
-
assert.equal(r.manifestState, STUB);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('maps validator INVALID → invalid-manifest and UNSUPPORTED → unsupported-schema', () => {
|
|
76
|
-
assert.equal(classifyMember(KIT, installedDeps({ report: { result: INVALID } })).manifestState, INVALID_MANIFEST);
|
|
77
|
-
assert.equal(classifyMember(KIT, installedDeps({ report: { result: UNSUPPORTED } })).manifestState, UNSUPPORTED_SCHEMA);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('surfaces an EACCES marker probe as "unknown" — never masked as not-installed', () => {
|
|
81
|
-
const eacces = () => { throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); };
|
|
82
|
-
const r = classifyMember(KIT, { exists: eacces, getenv: {}, home: '/home/test' });
|
|
83
|
-
assert.equal(r.manifestState, UNKNOWN);
|
|
84
|
-
assert.equal(r.installed, false); // not removed — ownership could not be verified
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('resolves the skill dir from the env override when set (resolveDir reuse)', () => {
|
|
88
|
-
const r = classifyMember(KIT, {
|
|
89
|
-
exists: () => true,
|
|
90
|
-
stat: () => ({ isFile: () => true }),
|
|
91
|
-
getenv: { AGENT_WORKFLOW_KIT_DIR: '/custom/kit' },
|
|
92
|
-
home: '/home/test',
|
|
93
|
-
validate: () => ({ result: VALID, name: 'agent-workflow-kit', kind: 'composition-root', available: true }),
|
|
94
|
-
readVersion: () => ({ version: '1.12.0' }),
|
|
95
|
-
});
|
|
96
|
-
assert.equal(r.skillDir, '/custom/kit');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
describe('surveyFamily', () => {
|
|
101
|
-
it('returns one row per member; all not-installed when no markers present', () => {
|
|
102
|
-
const rows = surveyFamily({ exists: () => false, getenv: {}, home: '/home/test' });
|
|
103
|
-
assert.equal(rows.length, FAMILY_MEMBERS.length);
|
|
104
|
-
assert.ok(rows.every((r) => r.manifestState === NOT_INSTALLED));
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
// Only the engine member validates as ok (its name+kind match); the others go FOREIGN under this
|
|
108
|
-
// shared validate stub — so only the engine row is eligible for the orchestration-fragment caveat.
|
|
109
|
-
const engineValidate = (dir) =>
|
|
110
|
-
String(dir).includes('agent-workflow-engine')
|
|
111
|
-
? { result: VALID, name: 'agent-workflow-engine', kind: 'methodology-engine', available: true }
|
|
112
|
-
: { result: VALID, name: 'x', kind: 'x', available: true };
|
|
113
|
-
|
|
114
|
-
// The caveats mirror the consumers: each reads a live engine fragment (readEngineFragment) — the
|
|
115
|
-
// recipes pointer (orchestration-slot.md, engine >= 1.2.0) AND the activity-procedures canon
|
|
116
|
-
// (procedures.md, engine >= 1.3.0). An absent / non-file / unreadable fragment surfaces as a DISTINCT
|
|
117
|
-
// caveat in `row.caveats` (an array, so missing BOTH surfaces BOTH); a current readable
|
|
118
|
-
// fragment never gets one. Helpers below model each fragment's on-disk state independently.
|
|
119
|
-
const engineDeps = (over) => ({
|
|
120
|
-
exists: () => true, // SKILL.md marker present (classifyMember)
|
|
121
|
-
stat: () => ({ isFile: () => true }),
|
|
122
|
-
getenv: {},
|
|
123
|
-
home: '/home/test',
|
|
124
|
-
validate: engineValidate,
|
|
125
|
-
readVersion: () => ({ version: '1.3.0' }),
|
|
126
|
-
...over,
|
|
127
|
-
});
|
|
128
|
-
// statType where each fragment is independently a readable file ('file') or absent (null); the engine
|
|
129
|
-
// dir + everything else is a 'dir'. readFileSync returns content for a present fragment.
|
|
130
|
-
// Pin the mock to the AUTHORITATIVE engine-source constants (mirrors engine-source.test.mjs), so a
|
|
131
|
-
// fragment-path rename follows here instead of silently passing against the old basename.
|
|
132
|
-
const fragmentStat = ({ orch = 'file', proc = 'file' }) => (p) => {
|
|
133
|
-
const s = String(p);
|
|
134
|
-
if (s.endsWith(ORCHESTRATION_FRAGMENT_REL)) return orch;
|
|
135
|
-
if (s.endsWith(PROCEDURES_FRAGMENT_REL)) return proc;
|
|
136
|
-
return 'dir';
|
|
137
|
-
};
|
|
138
|
-
const caveatsOf = (rows) => rows.find((r) => r.kind === 'methodology-engine').caveats ?? [];
|
|
139
|
-
|
|
140
|
-
it('a current engine WITH BOTH fragments readable carries NO caveat', () => {
|
|
141
|
-
const rows = surveyFamily(engineDeps({ statType: fragmentStat({}), readFileSync: () => '> a bounded fragment' }));
|
|
142
|
-
const engine = rows.find((r) => r.kind === 'methodology-engine');
|
|
143
|
-
assert.equal(engine.manifestState, OK);
|
|
144
|
-
assert.equal(engine.caveats, undefined, 'no caveats when both live fragments are present + readable');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('an OK engine MISSING the orchestration fragment gets the recipes caveat (only)', () => {
|
|
148
|
-
const rows = surveyFamily(engineDeps({
|
|
149
|
-
readVersion: () => ({ version: '1.2.0' }),
|
|
150
|
-
statType: fragmentStat({ orch: null }), // recipes fragment ABSENT, procedures present
|
|
151
|
-
readFileSync: () => '> a bounded fragment',
|
|
152
|
-
}));
|
|
153
|
-
const caveats = caveatsOf(rows);
|
|
154
|
-
assert.equal(caveats.length, 1);
|
|
155
|
-
assert.match(caveats[0], /recipes pointer/i);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('an OK engine MISSING the procedures canon gets the activity-procedures caveat (only)', () => {
|
|
159
|
-
// The realistic post-release case: an engine at 1.2.0 ships the recipes pointer but not procedures.md.
|
|
160
|
-
const rows = surveyFamily(engineDeps({
|
|
161
|
-
readVersion: () => ({ version: '1.2.0' }),
|
|
162
|
-
statType: fragmentStat({ proc: null }), // procedures canon ABSENT, recipes present
|
|
163
|
-
readFileSync: () => '> a bounded fragment',
|
|
164
|
-
}));
|
|
165
|
-
const caveats = caveatsOf(rows);
|
|
166
|
-
assert.equal(caveats.length, 1);
|
|
167
|
-
assert.match(caveats[0], /activity-procedures|procedures canon/i);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('an engine MISSING BOTH fragments surfaces BOTH caveats (neither overwrites the other)', () => {
|
|
171
|
-
const rows = surveyFamily(engineDeps({
|
|
172
|
-
readVersion: () => ({ version: '1.1.0' }),
|
|
173
|
-
statType: fragmentStat({ orch: null, proc: null }),
|
|
174
|
-
}));
|
|
175
|
-
const caveats = caveatsOf(rows);
|
|
176
|
-
assert.equal(caveats.length, 2, 'both missing fragments are reported');
|
|
177
|
-
assert.ok(caveats.some((c) => /recipes pointer/i.test(c)));
|
|
178
|
-
assert.ok(caveats.some((c) => /activity-procedures|procedures canon/i.test(c)));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
it('a broken engine whose fragments are DIRECTORIES is NOT a false "ok"', () => {
|
|
182
|
-
const rows = surveyFamily(engineDeps({ statType: () => 'dir' })); // every fragment path is a dir
|
|
183
|
-
assert.equal(caveatsOf(rows).length, 2, 'non-file fragments are caveated');
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('a fragment PRESENT but UNREADABLE is NOT a false "ok" (mirrors the consumer STOP)', () => {
|
|
187
|
-
const rows = surveyFamily(engineDeps({
|
|
188
|
-
statType: fragmentStat({}), // both present as files
|
|
189
|
-
readFileSync: () => {
|
|
190
|
-
throw Object.assign(new Error('EACCES'), { code: 'EACCES' }); // but unreadable
|
|
191
|
-
},
|
|
192
|
-
}));
|
|
193
|
-
assert.equal(caveatsOf(rows).length, 2, 'unreadable fragments are caveated, not reported clean');
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
// ── surveyProject ────────────────────────────────────────────────────────────────
|
|
198
|
-
|
|
199
|
-
describe('surveyProject', () => {
|
|
200
|
-
const projectDeps = ({ files }) => ({
|
|
201
|
-
exists: (p) => Object.prototype.hasOwnProperty.call(files, p) || Object.keys(files).some((k) => k === p),
|
|
202
|
-
readFile: (p) => {
|
|
203
|
-
if (!Object.prototype.hasOwnProperty.call(files, p)) throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
204
|
-
return files[p];
|
|
205
|
-
},
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('reports deployed + stamps + docs/ai + hidden fence', () => {
|
|
209
|
-
const dir = '/proj';
|
|
210
|
-
const files = {
|
|
211
|
-
[join(dir, 'docs/ai/.workflow-version')]: '1.3.0\n',
|
|
212
|
-
[join(dir, 'docs/ai/.memory-version')]: '1.1.1\n',
|
|
213
|
-
[join(dir, 'docs', 'ai')]: '',
|
|
214
|
-
[join(dir, '.git', 'info', 'exclude')]: `# user rule\n${START_MARKER}\n/AGENTS.md\n`,
|
|
215
|
-
};
|
|
216
|
-
const r = surveyProject(dir, projectDeps({ files }));
|
|
217
|
-
assert.equal(r.deployed, true);
|
|
218
|
-
assert.equal(r.docsAiPresent, true);
|
|
219
|
-
assert.equal(r.hiddenFence, true);
|
|
220
|
-
assert.deepEqual(
|
|
221
|
-
r.stamps.map((s) => [s.name, s.version]),
|
|
222
|
-
[['agent-workflow-kit', '1.3.0'], ['agent-workflow-memory', '1.1.1']],
|
|
223
|
-
);
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it('reports not-deployed when there is no docs/ai and no stamp', () => {
|
|
227
|
-
const r = surveyProject('/empty', projectDeps({ files: {} }));
|
|
228
|
-
assert.equal(r.deployed, false);
|
|
229
|
-
assert.equal(r.hiddenFence, false);
|
|
230
|
-
assert.ok(r.stamps.every((s) => s.version === null));
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// ── drift-guard: FAMILY_MEMBERS ⟷ the 5 in-repo capability.json (the AD-008 lockstep pattern) ──────
|
|
235
|
-
|
|
236
|
-
describe('FAMILY_MEMBERS drift-guard', () => {
|
|
237
|
-
const readManifest = (memberName) =>
|
|
238
|
-
JSON.parse(readFileSync(resolve(REPO_ROOT, memberName, 'capability.json'), 'utf8'));
|
|
239
|
-
|
|
240
|
-
// deduped roles[].cmd, in first-seen order (mirrors detect-backends' wrapperCmds derivation).
|
|
241
|
-
const dedupedCmds = (manifest) => {
|
|
242
|
-
const seen = new Set();
|
|
243
|
-
const out = [];
|
|
244
|
-
for (const role of Object.values(manifest.roles ?? {})) {
|
|
245
|
-
if (role && typeof role.cmd === 'string' && !seen.has(role.cmd)) {
|
|
246
|
-
seen.add(role.cmd);
|
|
247
|
-
out.push(role.cmd);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return out;
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
it('has exactly the five family members and no release skills', () => {
|
|
254
|
-
assert.equal(FAMILY_MEMBERS.length, 5);
|
|
255
|
-
const names = FAMILY_MEMBERS.map((m) => m.name);
|
|
256
|
-
assert.ok(!names.includes('release-engineering'));
|
|
257
|
-
assert.ok(!names.includes('release-marketing'));
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
for (const member of FAMILY_MEMBERS) {
|
|
261
|
-
it(`${member.name}: name/kind/detect.installed/deployed/npm/wrapperCmds match the in-repo manifest`, () => {
|
|
262
|
-
const m = readManifest(member.name);
|
|
263
|
-
assert.equal(m.name, member.name);
|
|
264
|
-
assert.equal(m.kind, member.kind);
|
|
265
|
-
|
|
266
|
-
assert.equal(m.detect.installed.env, member.installed.env);
|
|
267
|
-
assert.equal(m.detect.installed.default, member.installed.default);
|
|
268
|
-
assert.equal(m.detect.installed.file, member.installed.file);
|
|
269
|
-
|
|
270
|
-
if (member.deployed) assert.equal(m.detect.deployed.file, member.deployed.file);
|
|
271
|
-
else assert.equal(m.detect?.deployed ?? null, null);
|
|
272
|
-
|
|
273
|
-
if (member.npm) assert.equal(m.install.npm, member.npm);
|
|
274
|
-
else assert.equal(m.install?.npm ?? null, null);
|
|
275
|
-
|
|
276
|
-
assert.deepEqual(dedupedCmds(m), member.wrapperCmds);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
});
|