Files
nanoclaw/setup/provider-contract.test.ts
T
Omri Maya 13a37def89 feat(providers): operator-driven provider selection, switching, and memory migration
Make the agent provider a first-class, operator-chosen property instead of a
Claude-only assumption. Trunk gains the seams; the actual non-default payloads
(Codex first) install from the `providers` branch.

Setup
- A provider registry feeds a hard-wired setup picker (Claude | Codex). Picking
  a non-default provider installs its payload (setup/add-codex.sh, channel-style),
  runs a vault-only auth walkthrough (--step provider-auth), and records the pick
  on the first agent before its first spawn.
- Picking Claude changes nothing — default installs are byte-for-byte unaffected.

Provider as a DB property
- Provider lives on container_configs.provider (materialized to container.json,
  read by resolveProviderName). Creation stays provider-agnostic; the picked
  provider is applied via the picked-provider seam. The deprecated
  agent_groups.agent_provider path is not used.

Switching + memory
- Switch a live group with `ncl groups config update --provider` + restart.
- Memory never migrates at runtime — each provider keeps its own store. The
  /migrate-memory skill carries a group's memory across a switch in either
  direction (flat CLAUDE.local.md <-> memory/ scaffold). group-init seeds an
  imported-agent-memory note for non-default providers; the runner's memory
  definition reads it first turn. See docs/provider-migration.md.

No install-wide default, no runtime provider guard — switching is operator-by-
convention, consistent with the no-install-gating posture.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-14 07:49:39 +03:00

84 lines
3.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
/**
* Provider is a DB property of a group, set only via
* `ncl groups config update --provider`. The group-creation contract that a
* fork's coding agent and its skills depend on must carry zero provider
* vocabulary — no `--provider` flag passed to, parsed by, or threaded through
* any creation path. These guards go red if that flag creeps back in.
*
* (Prose references to the ncl surface in comments are fine — we assert the
* absence of the `'--provider'` arg *literal*, not the substring.)
*/
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
function read(rel: string): string {
return fs.readFileSync(path.join(repoRoot, rel), 'utf-8');
}
const CREATION_FILES = [
'scripts/init-first-agent.ts',
'scripts/init-cli-agent.ts',
'setup/register.ts',
'setup/cli-agent.ts',
'setup/channels/telegram.ts',
'setup/channels/discord.ts',
'setup/channels/slack.ts',
'setup/channels/whatsapp.ts',
'setup/channels/signal.ts',
'setup/channels/imessage.ts',
'setup/channels/teams.ts',
];
describe('creation is provider-agnostic', () => {
for (const file of CREATION_FILES) {
it(`${file} passes/parses no --provider flag`, () => {
const src = read(file);
expect(src).not.toContain("'--provider'");
expect(src).not.toMatch(/case '--provider'/);
});
}
});
describe('setup carries the picked provider to creation via a setup-run env var', () => {
it('picked-provider stashes/reads the pick in the NANOCLAW_PICKED_PROVIDER env var', () => {
const src = read('setup/lib/picked-provider.ts');
expect(src).toContain('NANOCLAW_PICKED_PROVIDER');
// The pick is set into process.env so child creation scripts inherit it —
// an in-process module global can't cross the process boundary.
expect(src).toMatch(/process\.env\[/);
});
// The creation scripts run as child processes, inherit the env var, and apply
// it to the group's runtime config — container_configs.provider, the source of
// truth materialized into container.json (agent_provider is deprecated) — before
// the welcome wakes the container. No `--provider` flag in the contract (above).
for (const file of ['scripts/init-first-agent.ts', 'scripts/init-cli-agent.ts']) {
it(`${file} applies the env-carried provider to container_configs.provider`, () => {
const src = read(file);
expect(src).toContain('NANOCLAW_PICKED_PROVIDER');
expect(src).toMatch(/updateContainerConfigScalars\([^)]*provider:\s*pickedProvider/);
});
}
});
describe('codex installs from a hard-wired self-contained script', () => {
// The provider picker no longer enumerates a remote manifest branch (an
// unaudited control surface). Codex is offered in trunk and installed by its
// own setup/add-<name>.sh, exactly like a channel adapter.
it('setup/add-codex.sh exists', () => {
expect(fs.existsSync(path.join(repoRoot, 'setup/add-codex.sh'))).toBe(true);
});
it('setup/auto.ts installs the picked provider by running setup/add-<name>.sh', () => {
const src = read('setup/auto.ts');
expect(src).toContain('setup/add-${agentProvider}.sh');
// The removed branch-enumeration machinery must not creep back in.
expect(src).not.toContain('listBranchProviderManifests');
expect(src).not.toContain('installProviderFromBranch');
});
});