@sabaiway/agent-workflow-kit 1.15.1 → 1.16.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/CHANGELOG.md +57 -0
- package/README.md +2 -1
- package/SKILL.md +91 -15
- package/capability.json +1 -1
- package/package.json +5 -2
- package/tools/commands.mjs +206 -0
- package/tools/delegation.mjs +8 -0
- package/tools/family-registry.mjs +263 -13
- package/tools/procedures.mjs +3 -1
- 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,444 +0,0 @@
|
|
|
1
|
-
import { describe, it } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync, mkdtempSync, writeFileSync, symlinkSync, chmodSync, readFileSync, readdirSync } from 'node:fs';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { join } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import {
|
|
8
|
-
expandTilde,
|
|
9
|
-
resolveDir,
|
|
10
|
-
findOnPath,
|
|
11
|
-
probeCredential,
|
|
12
|
-
detectBackend,
|
|
13
|
-
detectBackends,
|
|
14
|
-
formatReport,
|
|
15
|
-
guideFor,
|
|
16
|
-
KNOWN_BACKENDS,
|
|
17
|
-
} from './detect-backends.mjs';
|
|
18
|
-
|
|
19
|
-
const REPO = fileURLToPath(new URL('../../', import.meta.url)); // …/agent-workflow-kit/tools/ → repo root
|
|
20
|
-
const KIT = join(REPO, 'agent-workflow-kit');
|
|
21
|
-
const FIX = join(KIT, 'tools', 'manifest', 'fixtures');
|
|
22
|
-
const HOME = '/home/u';
|
|
23
|
-
|
|
24
|
-
// An ENOENT-typed error, matching the shape Node's fs throws (used to drive the wrapped probes).
|
|
25
|
-
const enoent = () => Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
|
|
26
|
-
const eacces = () => Object.assign(new Error('EACCES'), { code: 'EACCES' });
|
|
27
|
-
|
|
28
|
-
// ── pure helpers ─────────────────────────────────────────────────────────────
|
|
29
|
-
|
|
30
|
-
describe('expandTilde', () => {
|
|
31
|
-
it('"~" → home', () => assert.equal(expandTilde('~', HOME), HOME));
|
|
32
|
-
it('"~/x" → home/x', () => assert.equal(expandTilde('~/x/y', HOME), join(HOME, 'x/y')));
|
|
33
|
-
it('absolute path untouched', () => assert.equal(expandTilde('/abs/path', HOME), '/abs/path'));
|
|
34
|
-
it('relative path untouched', () => assert.equal(expandTilde('rel/path', HOME), 'rel/path'));
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('resolveDir', () => {
|
|
38
|
-
it('non-empty env wins, as-is', () => {
|
|
39
|
-
assert.equal(resolveDir({ env: 'D', default: '~/d' }, { D: '/from/env' }, HOME), '/from/env');
|
|
40
|
-
});
|
|
41
|
-
it('empty-string env → default', () => {
|
|
42
|
-
assert.equal(resolveDir({ env: 'D', default: '/d' }, { D: '' }, HOME), '/d');
|
|
43
|
-
});
|
|
44
|
-
it('null env name → default', () => {
|
|
45
|
-
assert.equal(resolveDir({ env: null, default: '/d' }, {}, HOME), '/d');
|
|
46
|
-
});
|
|
47
|
-
it('default is tilde-expanded', () => {
|
|
48
|
-
assert.equal(resolveDir({ env: 'D', default: '~/skills/x' }, {}, HOME), join(HOME, 'skills/x'));
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
describe('findOnPath', () => {
|
|
53
|
-
const linux = { platform: 'linux', getenv: { PATH: '/a:/b' }, realpath: (p) => p };
|
|
54
|
-
|
|
55
|
-
it('present via posix exec-bit', () => {
|
|
56
|
-
const r = findOnPath('codex', { ...linux, access: (p) => { if (p !== '/a/codex') throw enoent(); } });
|
|
57
|
-
assert.equal(r.state, 'present');
|
|
58
|
-
assert.equal(r.path, '/a/codex');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('missing when absent everywhere', () => {
|
|
62
|
-
const r = findOnPath('codex', { ...linux, access: () => { throw enoent(); } });
|
|
63
|
-
assert.equal(r.state, 'missing');
|
|
64
|
-
assert.equal(r.path, null);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('found in the 2nd PATH dir', () => {
|
|
68
|
-
const r = findOnPath('agy', { ...linux, access: (p) => { if (p !== '/b/agy') throw enoent(); } });
|
|
69
|
-
assert.equal(r.state, 'present');
|
|
70
|
-
assert.equal(r.path, '/b/agy');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('Windows: matches via PATHEXT', () => {
|
|
74
|
-
const r = findOnPath('codex', {
|
|
75
|
-
platform: 'win32',
|
|
76
|
-
getenv: { PATH: 'C:\\bin', PATHEXT: '.COM;.EXE;.CMD' },
|
|
77
|
-
realpath: (p) => p,
|
|
78
|
-
access: (p) => { if (!p.endsWith('codex.EXE')) throw enoent(); },
|
|
79
|
-
});
|
|
80
|
-
assert.equal(r.state, 'present');
|
|
81
|
-
assert.ok(r.path.endsWith('codex.EXE'));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('resolves a symlinked binary to its realpath (real fs)', () => {
|
|
85
|
-
const dir = mkdtempSync(join(tmpdir(), 'awf-path-'));
|
|
86
|
-
const target = join(dir, 'real-tool');
|
|
87
|
-
const link = join(dir, 'linked-tool');
|
|
88
|
-
writeFileSync(target, '#!/bin/sh\n');
|
|
89
|
-
chmodSync(target, 0o755);
|
|
90
|
-
symlinkSync(target, link);
|
|
91
|
-
const r = findOnPath('linked-tool', { platform: 'linux', getenv: { PATH: dir } });
|
|
92
|
-
assert.equal(r.state, 'present');
|
|
93
|
-
assert.equal(r.path, target); // realpath followed the symlink
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('accessSync throwing EACCES → unknown (cannot confirm)', () => {
|
|
97
|
-
const r = findOnPath('codex', { ...linux, access: () => { throw eacces(); } });
|
|
98
|
-
assert.equal(r.state, 'unknown');
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
describe('probeCredential', () => {
|
|
103
|
-
const entry = (over = {}) => ({ credential: { env: 'CRED', default: '~/.codex', file: 'auth.json', ...over } });
|
|
104
|
-
|
|
105
|
-
it('present when the marker file is a real file (env override honoured)', () => {
|
|
106
|
-
const r = probeCredential(entry(), { getenv: { CRED: '/cdir' }, home: HOME, exists: () => true, stat: () => ({ isFile: () => true }) });
|
|
107
|
-
assert.equal(r.state, 'present');
|
|
108
|
-
assert.equal(r.path, '/cdir/auth.json');
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('absent → missing', () => {
|
|
112
|
-
const r = probeCredential(entry(), { getenv: {}, home: HOME, exists: () => false });
|
|
113
|
-
assert.equal(r.state, 'missing');
|
|
114
|
-
assert.equal(r.path, join(HOME, '.codex/auth.json')); // env unset → tilde default
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('statSync throwing a non-ENOENT error → unknown', () => {
|
|
118
|
-
const r = probeCredential(entry(), { getenv: {}, home: HOME, exists: () => true, stat: () => { throw eacces(); } });
|
|
119
|
-
assert.equal(r.state, 'unknown');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('credential.env:null uses the (tilde-expanded) default', () => {
|
|
123
|
-
const r = probeCredential(entry({ env: null, default: '~/.gemini/antigravity-cli', file: 'tok' }), {
|
|
124
|
-
getenv: {}, home: HOME, exists: () => true, stat: () => ({ isFile: () => true }),
|
|
125
|
-
});
|
|
126
|
-
assert.equal(r.state, 'present');
|
|
127
|
-
assert.equal(r.path, join(HOME, '.gemini/antigravity-cli/tok'));
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('never counts a directory as a present credential', () => {
|
|
131
|
-
const r = probeCredential(entry(), { getenv: { CRED: '/cdir' }, home: HOME, exists: () => true, stat: () => ({ isFile: () => false }) });
|
|
132
|
-
assert.equal(r.state, 'missing');
|
|
133
|
-
});
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
// ── detectBackend: manifestState precedence ──────────────────────────────────
|
|
137
|
-
|
|
138
|
-
// Build an entry whose installed-marker is `capability.json` so a fixture without a SKILL.md still
|
|
139
|
-
// registers as "installed", letting the validator branch decide the state.
|
|
140
|
-
const entryAt = (dir, over = {}) => ({
|
|
141
|
-
name: 'codex-cli-bridge',
|
|
142
|
-
installed: { env: 'X_DIR', default: dir, file: 'capability.json' },
|
|
143
|
-
bin: 'codex',
|
|
144
|
-
credential: { env: null, default: '/no/such/dir', file: 'auth.json' },
|
|
145
|
-
setupUrl: 'https://example.test/setup',
|
|
146
|
-
setupPathLocal: 'setup/README.md',
|
|
147
|
-
...over,
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// All readiness inputs present, so manifestState is the only variable under test.
|
|
151
|
-
const allPresentDeps = {
|
|
152
|
-
getenv: {},
|
|
153
|
-
probeCli: () => ({ bin: 'codex', state: 'present', path: '/usr/bin/codex' }),
|
|
154
|
-
probeCredentials: () => ({ state: 'present', path: '/c/auth.json' }),
|
|
155
|
-
probeWrapper: (cmd) => ({ name: cmd, state: 'present' }),
|
|
156
|
-
};
|
|
157
|
-
|
|
158
|
-
describe('detectBackend — manifestState precedence', () => {
|
|
159
|
-
it('not-installed when the marker file is absent', () => {
|
|
160
|
-
const empty = mkdtempSync(join(tmpdir(), 'awf-empty-'));
|
|
161
|
-
const d = detectBackend(entryAt(empty), allPresentDeps);
|
|
162
|
-
assert.equal(d.manifestState, 'not-installed');
|
|
163
|
-
assert.equal(d.skillDir, null);
|
|
164
|
-
assert.equal(d.readiness, 'needs-skill');
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('unsupported-schema (fixtures/unknown-schema)', () => {
|
|
168
|
-
const d = detectBackend(entryAt(join(FIX, 'unknown-schema')), allPresentDeps);
|
|
169
|
-
assert.equal(d.manifestState, 'unsupported-schema');
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it('invalid-manifest (fixtures/malformed-json)', () => {
|
|
173
|
-
const d = detectBackend(entryAt(join(FIX, 'malformed-json')), allPresentDeps);
|
|
174
|
-
assert.equal(d.manifestState, 'invalid-manifest');
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it('stub (fixtures/stub — available:false)', () => {
|
|
178
|
-
const d = detectBackend(entryAt(join(FIX, 'stub')), allPresentDeps);
|
|
179
|
-
assert.equal(d.manifestState, 'stub');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('foreign (valid, but a memory-substrate, not this backend)', () => {
|
|
183
|
-
const d = detectBackend(entryAt(join(REPO, 'agent-workflow-memory')), allPresentDeps);
|
|
184
|
-
assert.equal(d.manifestState, 'foreign');
|
|
185
|
-
assert.match(d.manifestReason, /memory-substrate/);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('ok (the real codex-cli-bridge dir, marker = SKILL.md)', () => {
|
|
189
|
-
const d = detectBackend(
|
|
190
|
-
entryAt(join(REPO, 'codex-cli-bridge'), { installed: { env: 'X_DIR', default: join(REPO, 'codex-cli-bridge'), file: 'SKILL.md' } }),
|
|
191
|
-
allPresentDeps,
|
|
192
|
-
);
|
|
193
|
-
assert.equal(d.manifestState, 'ok');
|
|
194
|
-
assert.deepEqual(d.wrappers.map((w) => w.name).sort(), ['codex-exec', 'codex-review']);
|
|
195
|
-
assert.equal(d.setupHint.local, 'setup/README.md'); // installed AND setup/README.md exists
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
// ── detectBackend: readiness matrix (over an `ok` manifest) ───────────────────
|
|
200
|
-
|
|
201
|
-
const okEntry = entryAt(join(REPO, 'codex-cli-bridge'), {
|
|
202
|
-
installed: { env: 'X_DIR', default: join(REPO, 'codex-cli-bridge'), file: 'SKILL.md' },
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('detectBackend — readiness over an ok manifest', () => {
|
|
206
|
-
it('ready when cli + credentials + every wrapper present', () => {
|
|
207
|
-
assert.equal(detectBackend(okEntry, allPresentDeps).readiness, 'ready');
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it('needs-cli when the CLI is missing', () => {
|
|
211
|
-
const d = detectBackend(okEntry, { ...allPresentDeps, probeCli: () => ({ bin: 'codex', state: 'missing', path: null }) });
|
|
212
|
-
assert.equal(d.readiness, 'needs-cli');
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('needs-cli when the CLI is unknown (unknown never counts as present)', () => {
|
|
216
|
-
const d = detectBackend(okEntry, { ...allPresentDeps, probeCli: () => ({ bin: 'codex', state: 'unknown', path: null }) });
|
|
217
|
-
assert.equal(d.readiness, 'needs-cli');
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('needs-credentials when the credential marker is missing', () => {
|
|
221
|
-
const d = detectBackend(okEntry, { ...allPresentDeps, probeCredentials: () => ({ state: 'missing', path: '/c/auth.json' }) });
|
|
222
|
-
assert.equal(d.readiness, 'needs-credentials');
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('degraded when a wrapper is missing from PATH', () => {
|
|
226
|
-
const d = detectBackend(okEntry, {
|
|
227
|
-
...allPresentDeps,
|
|
228
|
-
probeWrapper: (cmd) => ({ name: cmd, state: cmd === 'codex-review' ? 'missing' : 'present' }),
|
|
229
|
-
});
|
|
230
|
-
assert.equal(d.readiness, 'degraded');
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('needs-skill dominates even when cli + credentials are present', () => {
|
|
234
|
-
const empty = mkdtempSync(join(tmpdir(), 'awf-empty2-'));
|
|
235
|
-
const d = detectBackend(entryAt(empty), allPresentDeps);
|
|
236
|
-
assert.equal(d.readiness, 'needs-skill');
|
|
237
|
-
});
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// ── registry drift guard ─────────────────────────────────────────────────────
|
|
241
|
-
|
|
242
|
-
const readManifest = (dir) => JSON.parse(readFileSync(join(dir, 'capability.json'), 'utf8'));
|
|
243
|
-
|
|
244
|
-
// Every top-level in-repo dir with a kind:execution-backend manifest, discovered fresh from disk.
|
|
245
|
-
const inRepoBackends = () =>
|
|
246
|
-
readdirSync(REPO, { withFileTypes: true })
|
|
247
|
-
.filter((e) => e.isDirectory() && existsSync(join(REPO, e.name, 'capability.json')))
|
|
248
|
-
.filter((e) => readManifest(join(REPO, e.name)).kind === 'execution-backend')
|
|
249
|
-
.map((e) => e.name);
|
|
250
|
-
|
|
251
|
-
describe('KNOWN_BACKENDS — drift guard against the in-repo manifests', () => {
|
|
252
|
-
it('set equality: registry names == in-repo execution-backend dirs', () => {
|
|
253
|
-
const onDisk = inRepoBackends().sort();
|
|
254
|
-
const registry = KNOWN_BACKENDS.map((b) => b.name).sort();
|
|
255
|
-
assert.deepEqual(registry, onDisk);
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('the methodology-engine is NOT counted as a backend', () => {
|
|
259
|
-
assert.ok(!KNOWN_BACKENDS.some((b) => b.name === 'agent-workflow-engine'));
|
|
260
|
-
assert.equal(readManifest(join(REPO, 'agent-workflow-engine')).kind, 'methodology-engine');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('registry names are unique', () => {
|
|
264
|
-
const names = KNOWN_BACKENDS.map((b) => b.name);
|
|
265
|
-
assert.equal(new Set(names).size, names.length);
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('each entry.installed matches the real manifest detect.installed', () => {
|
|
269
|
-
for (const entry of KNOWN_BACKENDS) {
|
|
270
|
-
const real = readManifest(join(REPO, entry.name)).detect.installed;
|
|
271
|
-
assert.equal(entry.installed.env, real.env, `${entry.name} env`);
|
|
272
|
-
assert.equal(entry.installed.default, real.default, `${entry.name} default`);
|
|
273
|
-
assert.equal(entry.installed.file, real.file, `${entry.name} file`);
|
|
274
|
-
}
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('each entry.setupPathLocal exists in the repo', () => {
|
|
278
|
-
for (const entry of KNOWN_BACKENDS) {
|
|
279
|
-
assert.ok(existsSync(join(REPO, entry.name, entry.setupPathLocal)), `${entry.name}/${entry.setupPathLocal}`);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
// ── formatReport ─────────────────────────────────────────────────────────────
|
|
285
|
-
|
|
286
|
-
const readyStatus = {
|
|
287
|
-
name: 'codex-cli-bridge',
|
|
288
|
-
manifestState: 'ok',
|
|
289
|
-
manifestReason: 'ok',
|
|
290
|
-
skillDir: '/skills/codex-cli-bridge',
|
|
291
|
-
cli: { bin: 'codex', state: 'present', path: '/usr/bin/codex' },
|
|
292
|
-
credentials: { state: 'present', path: '/home/u/.codex/auth.json' },
|
|
293
|
-
wrappers: [{ name: 'codex-exec', state: 'present' }, { name: 'codex-review', state: 'present' }],
|
|
294
|
-
readiness: 'ready',
|
|
295
|
-
setupHint: { local: 'setup/README.md', url: 'https://example.test/codex' },
|
|
296
|
-
};
|
|
297
|
-
const needsSkillStatus = {
|
|
298
|
-
name: 'antigravity-cli-bridge',
|
|
299
|
-
manifestState: 'not-installed',
|
|
300
|
-
manifestReason: 'not installed',
|
|
301
|
-
skillDir: null,
|
|
302
|
-
cli: { bin: 'agy', state: 'present', path: '/home/u/.local/bin/agy' },
|
|
303
|
-
credentials: { state: 'present', path: '/home/u/.gemini/antigravity-cli/antigravity-oauth-token' },
|
|
304
|
-
wrappers: [],
|
|
305
|
-
readiness: 'needs-skill',
|
|
306
|
-
setupHint: { url: 'https://example.test/agy' },
|
|
307
|
-
};
|
|
308
|
-
|
|
309
|
-
describe('formatReport', () => {
|
|
310
|
-
const out = formatReport([readyStatus, needsSkillStatus]);
|
|
311
|
-
|
|
312
|
-
it('never says "authenticated" or "authed" (credentials = file presence, not a live login)', () => {
|
|
313
|
-
const low = out.toLowerCase();
|
|
314
|
-
assert.ok(!low.includes('authenticated'));
|
|
315
|
-
assert.ok(!low.includes('authed'));
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
it('uses the word "credentials"', () => assert.ok(out.toLowerCase().includes('credentials')));
|
|
319
|
-
|
|
320
|
-
it('shows the ready backend as ready', () => {
|
|
321
|
-
assert.match(out, /codex-cli-bridge.*ready/s);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
it('a needs-skill backend (CLI + creds present) says install the bridge + points at the setup URL', () => {
|
|
325
|
-
assert.match(out, /needs-skill/);
|
|
326
|
-
assert.match(out, /install the bridge/);
|
|
327
|
-
assert.match(out, /https:\/\/example\.test\/agy/);
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
describe('detectBackends — live shape on this machine', () => {
|
|
332
|
-
it('returns one status per registry entry with all data-model keys', () => {
|
|
333
|
-
const statuses = detectBackends();
|
|
334
|
-
assert.equal(statuses.length, KNOWN_BACKENDS.length);
|
|
335
|
-
for (const s of statuses) {
|
|
336
|
-
for (const key of ['name', 'manifestState', 'manifestReason', 'cli', 'credentials', 'wrappers', 'readiness', 'setupHint']) {
|
|
337
|
-
assert.ok(key in s, `${s.name} missing ${key}`);
|
|
338
|
-
}
|
|
339
|
-
assert.ok(['present', 'missing', 'unknown'].includes(s.cli.state));
|
|
340
|
-
assert.ok(['present', 'missing', 'unknown'].includes(s.credentials.state));
|
|
341
|
-
}
|
|
342
|
-
});
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
// ── guideFor (axis-aware manual steps) ────────────────────────────────────────
|
|
346
|
-
|
|
347
|
-
// A status with all axes satisfied; override per-case to drive each axis independently.
|
|
348
|
-
const okStatus = (over = {}) => ({
|
|
349
|
-
name: 'codex-cli-bridge',
|
|
350
|
-
manifestState: 'ok',
|
|
351
|
-
manifestReason: 'ok',
|
|
352
|
-
skillDir: '/skills/codex-cli-bridge',
|
|
353
|
-
cli: { bin: 'codex', state: 'present', path: '/usr/bin/codex' },
|
|
354
|
-
credentials: { state: 'present', path: '/home/u/.codex/auth.json' },
|
|
355
|
-
wrappers: [{ name: 'codex-exec', state: 'present' }, { name: 'codex-review', state: 'present' }],
|
|
356
|
-
readiness: 'ready',
|
|
357
|
-
setupHint: { local: 'setup/README.md', url: 'https://example.test/codex' },
|
|
358
|
-
...over,
|
|
359
|
-
});
|
|
360
|
-
|
|
361
|
-
describe('guideFor — axis-aware manual steps', () => {
|
|
362
|
-
it('returns [] when the backend is ready', () => {
|
|
363
|
-
assert.deepEqual(guideFor(okStatus()), []);
|
|
364
|
-
});
|
|
365
|
-
|
|
366
|
-
it('returns [] for a degraded backend (wrappers are the linker\'s job, not a manual step)', () => {
|
|
367
|
-
const s = okStatus({ readiness: 'degraded', wrappers: [{ name: 'codex-exec', state: 'missing' }] });
|
|
368
|
-
assert.deepEqual(guideFor(s), []);
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('returns BOTH cli and credentials steps when both are missing (multiple simultaneous)', () => {
|
|
372
|
-
const s = okStatus({
|
|
373
|
-
cli: { bin: 'codex', state: 'missing', path: null },
|
|
374
|
-
credentials: { state: 'missing', path: '/c/auth.json' },
|
|
375
|
-
});
|
|
376
|
-
const needs = guideFor(s).map((g) => g.need);
|
|
377
|
-
assert.deepEqual(needs, ['cli', 'credentials']);
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('credentials step carries the canonical loginCmd + verifyCmd from the registry', () => {
|
|
381
|
-
const s = okStatus({ credentials: { state: 'missing', path: '/c/auth.json' } });
|
|
382
|
-
const step = guideFor(s).find((g) => g.need === 'credentials');
|
|
383
|
-
assert.match(step.hint, /codex login/);
|
|
384
|
-
assert.match(step.hint, /codex login status/);
|
|
385
|
-
});
|
|
386
|
-
|
|
387
|
-
it('cli step references the setup README (no duplicated install channels)', () => {
|
|
388
|
-
const s = okStatus({ cli: { bin: 'codex', state: 'missing', path: null } });
|
|
389
|
-
const step = guideFor(s).find((g) => g.need === 'cli');
|
|
390
|
-
assert.match(step.hint, /codex-cli-bridge\/setup\/README\.md/);
|
|
391
|
-
});
|
|
392
|
-
|
|
393
|
-
it('manifestState not-installed → a placeable bundled-skill hint (+ any cli/creds owed)', () => {
|
|
394
|
-
const s = okStatus({ manifestState: 'not-installed', skillDir: null });
|
|
395
|
-
const skill = guideFor(s).find((g) => g.need === 'skill');
|
|
396
|
-
assert.ok(skill, 'expected a skill step');
|
|
397
|
-
assert.match(skill.hint, /bundled bridge skill/);
|
|
398
|
-
assert.match(skill.hint, /setup codex-cli-bridge/);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('manifestState foreign/stub → a STOP hint (never auto-overwritten)', () => {
|
|
402
|
-
for (const state of ['foreign', 'stub', 'invalid-manifest', 'unsupported-schema', 'unknown']) {
|
|
403
|
-
const skill = guideFor(okStatus({ manifestState: state })).find((g) => g.need === 'skill');
|
|
404
|
-
assert.ok(skill, `expected a skill step for ${state}`);
|
|
405
|
-
assert.match(skill.hint, /STOP/);
|
|
406
|
-
assert.match(skill.hint, new RegExp(state.replace(/[-]/g, '\\$&')));
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
it('every step is {need, hint} with a non-empty hint', () => {
|
|
411
|
-
const s = okStatus({
|
|
412
|
-
manifestState: 'not-installed',
|
|
413
|
-
skillDir: null,
|
|
414
|
-
cli: { bin: 'codex', state: 'missing', path: null },
|
|
415
|
-
credentials: { state: 'missing', path: '/c/auth.json' },
|
|
416
|
-
});
|
|
417
|
-
const steps = guideFor(s);
|
|
418
|
-
assert.equal(steps.length, 3);
|
|
419
|
-
for (const step of steps) {
|
|
420
|
-
assert.ok(typeof step.need === 'string' && step.need.length > 0);
|
|
421
|
-
assert.ok(typeof step.hint === 'string' && step.hint.length > 0);
|
|
422
|
-
}
|
|
423
|
-
});
|
|
424
|
-
});
|
|
425
|
-
|
|
426
|
-
describe('KNOWN_BACKENDS — each entry exposes a guide', () => {
|
|
427
|
-
it('guide carries setupRef + loginCmd + verifyCmd (non-empty strings)', () => {
|
|
428
|
-
for (const entry of KNOWN_BACKENDS) {
|
|
429
|
-
assert.ok(entry.guide, `${entry.name} missing guide`);
|
|
430
|
-
for (const key of ['setupRef', 'loginCmd', 'verifyCmd']) {
|
|
431
|
-
assert.ok(
|
|
432
|
-
typeof entry.guide[key] === 'string' && entry.guide[key].length > 0,
|
|
433
|
-
`${entry.name}.guide.${key} must be a non-empty string`,
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
it('guide.setupRef points at a real file in the repo', () => {
|
|
440
|
-
for (const entry of KNOWN_BACKENDS) {
|
|
441
|
-
assert.ok(existsSync(join(REPO, entry.guide.setupRef)), `${entry.guide.setupRef} (${entry.name})`);
|
|
442
|
-
}
|
|
443
|
-
});
|
|
444
|
-
});
|