diff --git a/docs/ddd/component-directory.md b/docs/ddd/component-directory.md index 458904f5..d571ba7a 100644 --- a/docs/ddd/component-directory.md +++ b/docs/ddd/component-directory.md @@ -161,8 +161,12 @@ takes the worst of the pair, so the quieter one's health cannot hide behind the no endpoint, and a failed status join degrades chips to `unknown` without hiding cards. 3. **Prose never claims runtime state.** Installed/version/configured render exclusively as chips fed by detection; the paragraph reads true on any machine. -4. **Registry↔directory parity is a test.** Every managed tool has exactly one entry; no entry - exists for something ak neither installs nor configures. +4. **Registry↔directory parity is a test, scoped to built-in adapters.** Every managed tool + shipped in the built-in registry has exactly one entry; no entry exists for something ak + neither installs nor configures. There is no dynamic or third-party host concept yet, so + today "built-in" and "the registry" are the same set; an externally-admitted adapter is + exempt from this parity gate until the wave-4 adapter-extension contract graduates it to a + card-carrying citizen. 5. **Links are `https`, named-host, user-initiated**; the kit fetches none of them; all are covered by the nightly external link sweep. 6. **Official marks only where genuinely official and already shipped**; everything else is an diff --git a/tests/kit/about-directory.test.mjs b/tests/kit/about-directory.test.mjs index 8a534b59..987110c9 100644 --- a/tests/kit/about-directory.test.mjs +++ b/tests/kit/about-directory.test.mjs @@ -106,6 +106,14 @@ function shippedCommands() { // --------------------------------------------------------------------------------------- // The parity gate (ADR-0026 §3, component-directory invariant 4) +// +// Scope: every assertion below walks HOST_REGISTRY / managedTools(), i.e. the BUILT-IN +// adapter set — there is no dynamic or third-party host concept yet (see +// src/lib/adapters/lifecycle-registry.mjs), so "built-in" and "the registry" are the same +// set today. That makes this gate built-in-scoped by construction: an externally-admitted +// adapter (wave-4 adapter-extension contract) is exempt from directory-card parity until +// that contract graduates it into a card-carrying citizen — it will not appear in +// HOST_REGISTRY / managedTools() until it does, so it cannot trip these assertions. // --------------------------------------------------------------------------------------- test('every managed tool has exactly one directory entry (registry → directory)', () => { diff --git a/tests/kit/hosts.test.mjs b/tests/kit/hosts.test.mjs index ffd56d88..9561cdc5 100644 --- a/tests/kit/hosts.test.mjs +++ b/tests/kit/hosts.test.mjs @@ -5,10 +5,15 @@ import os from 'node:os'; import path from 'node:path'; import { HOST_IDS, adapterFor, drivingHost } from '../../src/lib/hosts.mjs'; import { hostAuthState } from '../../src/lib/providers.mjs'; +import { managedHostIds } from '../../src/lib/adapters/registries.mjs'; // ── HOST_ADAPTERS descriptors ──────────────────────────────────────────────── -test('HOST_ADAPTERS defines the three hosts', () => { - assert.deepEqual(HOST_IDS, ['claude', 'codex', 'opencode']); +// HOST_IDS is HOST_ADAPTERS' key order (hosts.mjs filters HOST_REGISTRY by +// canDriveSession and preserves registry order) — this proves that mirror, +// not the exact built-in set (adapter-registries.test.mjs pins the full +// HOST_REGISTRY contents deep-equal; that's where the shipped-set literal lives). +test('HOST_ADAPTERS keys mirror the registry-derived managed host ids, in order', () => { + assert.deepEqual(HOST_IDS, managedHostIds()); }); test('claude adapter targets CLAUDE.md/json and supports a statusline', () => { diff --git a/tests/kit/integration-config.test.mjs b/tests/kit/integration-config.test.mjs index 7cebf958..62558a16 100644 --- a/tests/kit/integration-config.test.mjs +++ b/tests/kit/integration-config.test.mjs @@ -7,6 +7,7 @@ import { } from '../../src/lib/adapters/config.mjs'; import { migrateConfig } from '../../src/lib/adapters/migration.mjs'; import { migrateKitConfig } from '../../src/lib/config.mjs'; +import { defaultHostMap } from '../../src/lib/adapters/index.mjs'; const legacyDual = { providers: { @@ -171,7 +172,11 @@ test('active legacy hosts win once over a stale additive integration snapshot', providers: { hosts: { claude: false, codex: true, opencode: true } }, integrations: { version: 1, - hosts: { claude: true, codex: false, opencode: false }, + // The stale snapshot is deliberately the fresh-install defaults, contrasted with the + // active providers.hosts above (its exact inverse) — the point is precedence, not + // this literal shape, so derive it rather than hand-typing a value that would silently + // stop being "a plausible stale default" if the registry's defaults ever changed. + hosts: defaultHostMap(), bindings: [], }, }); diff --git a/tests/kit/provider-cli.test.mjs b/tests/kit/provider-cli.test.mjs index f77619fb..24ce7d33 100644 --- a/tests/kit/provider-cli.test.mjs +++ b/tests/kit/provider-cli.test.mjs @@ -13,6 +13,7 @@ import fs from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { DUAL_ROLE_TIP, JUDGE_BIAS_TIP } from '../../src/lib/providers.mjs'; +import { defaultHostMap } from '../../src/lib/adapters/index.mjs'; // Tripwire (#137): a spawned `ak x host pick` whose cwd falls back to the test // process's cwd writes PROJECT-scoped config (.claude/settings.local.json, @@ -239,7 +240,7 @@ test('pick --host claude,opencode enables + wires opencode (config, plugin, agen 'enablement-gated guidance converges on enable — "wired + guided" is one contract'); const cfg = kitJson(sb.home); - assert.deepEqual(cfg.integrations.hosts, { claude: true, codex: false, opencode: true }); + assert.deepEqual(cfg.integrations.hosts, { ...defaultHostMap(), opencode: true }); assert.equal(cfg.integrations.ownership.opencode.mcp, 'ak', 'ownership marker persisted'); assert.ok(cfg.integrations.ownership.opencode.managed?.mcp?.['claude-flow']?.written, 'value-precise ownership recorded'); } finally { @@ -276,7 +277,7 @@ test('pick --host claude on an opencode-enabled machine disables it: ak wiring s 'ak plugin removed'); const cfg = kitJson(sb.home); - assert.deepEqual(cfg.integrations.hosts, { claude: true, codex: false, opencode: false }); + assert.deepEqual(cfg.integrations.hosts, defaultHostMap()); assert.equal(cfg.integrations.ownership.opencode.mcp, null, 'ownership markers nulled on disable'); assert.equal(cfg.integrations.ownership.opencode.managed, null); } finally { diff --git a/tests/kit/providers.test.mjs b/tests/kit/providers.test.mjs index 57284e6c..fd6059a9 100644 --- a/tests/kit/providers.test.mjs +++ b/tests/kit/providers.test.mjs @@ -13,7 +13,7 @@ import { PROVIDER_TOKEN_RE, seedActivityRoutesIfMultiHost, } from '../../src/lib/providers.mjs'; import * as paths from '../../src/lib/paths.mjs'; -import { managedHostIds, routableHostIds } from '../../src/lib/adapters/index.mjs'; +import { managedHostIds, routableHostIds, HOST_REGISTRY } from '../../src/lib/adapters/index.mjs'; // security review Finding 2 / code-quality Finding 2: applyProviders() feeds // m.id/m.model into a ruflo subprocess argv. exec.mjs's shell:false fix is @@ -220,8 +220,15 @@ test('every host descriptor carries an npm package name for install/update', () }); test('managed and routable host sets come only from registry capabilities', () => { - assert.deepEqual(managedHostIds(), ['claude', 'codex', 'opencode']); - assert.deepEqual(routableHostIds(), ['claude', 'codex', 'opencode']); + // Derive expectations straight from HOST_REGISTRY.capabilities (not a hand-typed + // id list) so the assertion actually proves the claim in the test name; the + // shipped built-in set itself is pinned deep-equal in adapter-registries.test.mjs. + const expectedManaged = HOST_REGISTRY.filter((h) => h.capabilities.canDriveSession).map((h) => h.id); + const expectedRoutable = HOST_REGISTRY.filter((h) => h.capabilities.canRouteActivities).map((h) => h.id); + assert.deepEqual(managedHostIds(), expectedManaged); + assert.deepEqual(routableHostIds(), expectedRoutable); + // HOSTS (providers.mjs) filters HOST_REGISTRY independently of managedHostIds() — + // pin that the two derivations agree. assert.deepEqual(HOSTS.map((h) => h.id), managedHostIds()); }); diff --git a/tests/kit/routing-config.test.mjs b/tests/kit/routing-config.test.mjs index babd30cd..1eeb88d6 100644 --- a/tests/kit/routing-config.test.mjs +++ b/tests/kit/routing-config.test.mjs @@ -10,6 +10,7 @@ import { routingIntent, } from '../../src/lib/routing-config.mjs'; import { loadKitConfig, saveKitConfig } from '../../src/lib/config.mjs'; +import { defaultHostMap } from '../../src/lib/adapters/index.mjs'; test('legacy routes migrate without resolving absence and preserve an explicit empty ladder', () => { const legacy = { @@ -159,8 +160,7 @@ test('load migrates raw legacy presence without writing; save persists only cano const before = fs.readFileSync(file, 'utf8'); const loaded = loadKitConfig(file); assert.equal(fs.readFileSync(file, 'utf8'), before, 'load is read-only'); - assert.deepEqual(loaded.integrations.hosts, - { claude: true, codex: true, opencode: false }); + assert.deepEqual(loaded.integrations.hosts, { ...defaultHostMap(), codex: true }); assert.equal(loaded.routing.primaryHost, 'codex'); assert.deepEqual(loaded.routing.routes.implementation.escalation, []); assert.equal(loaded.providers.aqeProvider, 'openai'); diff --git a/tests/kit/routing-primary.test.mjs b/tests/kit/routing-primary.test.mjs index a6d34be6..a8cf7551 100644 --- a/tests/kit/routing-primary.test.mjs +++ b/tests/kit/routing-primary.test.mjs @@ -1,6 +1,7 @@ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { swapHostModel, swapRoute, seedActivityRoutes, DEFAULT_ROUTES, DEFAULT_PRIMARY_HOST, PRIMARY_HOSTS } from '../../src/lib/routing.mjs'; +import { primaryHostIds } from '../../src/lib/adapters/index.mjs'; // ── swapHostModel ──────────────────────────────────────────────────────────── test('swapHostModel maps a claude model to the codex host', () => { @@ -42,7 +43,10 @@ test('seedActivityRoutes stamps every seeded entry with provenance:seeded', () = }); // ── constants ──────────────────────────────────────────────────────────────── -test('DEFAULT_PRIMARY_HOST is claude and both hosts are valid primaries', () => { - assert.equal(DEFAULT_PRIMARY_HOST, 'claude'); - assert.deepEqual([...PRIMARY_HOSTS].sort(), ['claude', 'codex']); +test('DEFAULT_PRIMARY_HOST is claude and PRIMARY_HOSTS mirrors the registry\'s canBePrimary set', () => { + assert.equal(DEFAULT_PRIMARY_HOST, 'claude'); // a policy choice, not derived from the registry + // PRIMARY_HOSTS must stay green under capability caps — a host gaining canBePrimary:true + // should extend this automatically, so derive the expectation from primaryHostIds() + // instead of a hand-typed host list. + assert.deepEqual([...PRIMARY_HOSTS].sort(), [...primaryHostIds()].sort()); }); diff --git a/tests/kit/setup-host-flags.test.mjs b/tests/kit/setup-host-flags.test.mjs index 9a760ff3..1ad3b614 100644 --- a/tests/kit/setup-host-flags.test.mjs +++ b/tests/kit/setup-host-flags.test.mjs @@ -5,6 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { applySetupHostFlags } from '../../src/lib/providers.mjs'; import { loadKitConfig, saveKitConfig } from '../../src/lib/config.mjs'; +import { defaultHostMap } from '../../src/lib/adapters/index.mjs'; const freshCfg = () => ({ integrations: { @@ -94,11 +95,7 @@ test('an empty config gets complete canonical envelopes and survives persistence routes: {}, }); assert.equal(cfg.integrations.version, 2); - assert.deepEqual(cfg.integrations.hosts, { - claude: true, - codex: true, - opencode: false, - }); + assert.deepEqual(cfg.integrations.hosts, { ...defaultHostMap(), codex: true }); const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ak-setup-empty-')); const file = path.join(dir, 'kit.json');