Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/ddd/component-directory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions tests/kit/about-directory.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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)', () => {
Expand Down
9 changes: 7 additions & 2 deletions tests/kit/hosts.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
7 changes: 6 additions & 1 deletion tests/kit/integration-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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: [],
},
});
Expand Down
5 changes: 3 additions & 2 deletions tests/kit/provider-cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down
13 changes: 10 additions & 3 deletions tests/kit/providers.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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());
});

Expand Down
4 changes: 2 additions & 2 deletions tests/kit/routing-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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');
Expand Down
10 changes: 7 additions & 3 deletions tests/kit/routing-primary.test.mjs
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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());
});
7 changes: 2 additions & 5 deletions tests/kit/setup-host-flags.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -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');
Expand Down
Loading