Files
gstack/test/host-config.test.ts
T
Garry Tan b805aa0113 feat: Confusion Protocol, Hermes + GBrain hosts, brain-first resolver (v0.18.0.0) (#1005)
* feat: add Confusion Protocol to preamble resolver

Injects a high-stakes ambiguity gate at preamble tier >= 2 so all
workflow skills get it. Fires when Claude encounters architectural
decisions, data model changes, destructive operations, or contradictory
requirements. Does NOT fire on routine coding.

Addresses Karpathy failure mode #1 (wrong assumptions) with an
inline STOP gate instead of relying on workflow skill invocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add Hermes and GBrain host configs

Hermes: tool rewrites for terminal/read_file/patch/delegate_task,
paths to ~/.hermes/skills/gstack, AGENTS.md config file.

GBrain: coding skills become brain-aware when GBrain mod is installed.
Same tool rewrites as OpenClaw (agents spawn Claude Code via ACP).
GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS NOT suppressed on gbrain
host, enabling brain-first lookup and save-to-brain behavior.

Both registered in hosts/index.ts with setup script redirect messages.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: GBrain resolver — brain-first lookup and save-to-brain

New scripts/resolvers/gbrain.ts with two resolver functions:
- GBRAIN_CONTEXT_LOAD: search brain for context before skill starts
- GBRAIN_SAVE_RESULTS: save skill output to brain after completion

Placeholders added to 4 thinking skill templates (office-hours,
investigate, plan-ceo-review, retro). Resolves to empty string on
all hosts except gbrain via suppressedResolvers.

GBRAIN suppression added to all 9 non-gbrain host configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: wire slop:diff into /review as advisory diagnostic

Adds Step 3.5 to the review template: runs bun run slop:diff against
the base branch to catch AI code quality issues (empty catches,
redundant return await, overcomplicated abstractions). Advisory only,
never blocking. Skips silently if slop-scan is not installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: add Karpathy compatibility note to README

Positions gstack as the workflow enforcement layer for Karpathy-style
CLAUDE.md rules (17K stars). Links to forrestchang/andrej-karpathy-skills.
Maps each Karpathy failure mode to the gstack skill that addresses it.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: improve native OpenClaw thinking skills

office-hours: add design doc path visibility message after writing
ceo-review: add HARD GATE reminder at review section transitions
retro: add non-git context support (check memory for meeting notes)

Mirrors template improvements to hand-crafted native skills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: update tests and golden fixtures for new hosts

- Host count: 8 → 10 (hermes, gbrain)
- OpenClaw adapter test: expects undefined (dead code removed)
- Golden ship fixtures: updated with Confusion Protocol + vendoring

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate all SKILL.md files

Regenerated from templates after Confusion Protocol, GBrain resolver
placeholders, slop:diff in review, HARD GATE reminders, investigation
learnings, design doc visibility, and retro non-git context changes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.18.0.0

- CHANGELOG: add v0.18.0.0 entry (Confusion Protocol, Hermes, GBrain,
  slop in review, Karpathy note, skill improvements)
- CLAUDE.md: add hermes.ts and gbrain.ts to hosts listing
- README.md: update agent count 8→10, add Hermes + GBrain to table
- VERSION: bump to 0.18.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: sync package.json version to 0.18.0.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: extract Step 0 from review SKILL.md in E2E test

The review-base-branch E2E test was copying the full 1493-line
review/SKILL.md into the test fixture. The agent spent 8+ turns
reading it in chunks, leaving only 7 turns for actual work, causing
error_max_turns on every attempt.

Now extracts only Step 0 (base branch detection, ~50 lines) which is
all the test actually needs. Follows the CLAUDE.md rule: "NEVER copy
a full SKILL.md file into an E2E test fixture."

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: update GBrain and Hermes host configs for v0.10.0 integration

GBrain: add 'triggers' to keepFields so generated skills pass
checkResolvable() validation. Add version compat comment.

Hermes: un-suppress GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS.
The resolvers handle GBrain-not-installed gracefully, so Hermes
agents with GBrain as a mod get brain features automatically.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: GBrain resolver DX improvements and preamble health check

Resolver changes:
- gbrain query → gbrain search (fast keyword search, not expensive hybrid)
- Add keyword extraction guidance for agents
- Show explicit gbrain put_page syntax with --title, --tags, heredoc
- Add entity enrichment with false-positive filter
- Name throttle error patterns (exit code 1, stderr keywords)
- Add data-research routing for investigate skill
- Expand skillSaveMap from 4 to 8 entries
- Add brain operation telemetry summary

Preamble changes:
- Add gbrain doctor --fast --json health check for gbrain/hermes hosts
- Parse check failures/warnings count
- Show failing check details when score < 50

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: preserve keepFields in allowlist frontmatter mode

The allowlist mode hard-coded name + description reconstruction but
never iterated keepFields for additional fields. Adding 'triggers'
to keepFields was a no-op because the field was silently stripped.

Now iterates keepFields and preserves any field beyond name/description
from the source template frontmatter, including YAML arrays.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add triggers to all 38 skill templates

Multi-word, skill-specific trigger keywords for GBrain's RESOLVER.md
router. Each skill gets 3-6 triggers derived from its "Use when asked
to..." description text. Avoids single generic words that would collide
across skills (e.g., "debug this" not "debug").

These are distinct from voice-triggers (speech-to-text aliases) and
serve GBrain's checkResolvable() validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: regenerate all SKILL.md files and update golden fixtures

Regenerated from updated templates (triggers, brain placeholders,
resolver DX improvements, preamble health check). Golden fixtures
updated to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: settings-hook remove exits 1 when nothing to remove

gstack-settings-hook remove was exiting 0 when settings.json didn't
exist, causing gstack-uninstall to report "SessionStart hook" as
removed on clean systems where nothing was installed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs: update project documentation for GBrain v0.10.0 integration

ARCHITECTURE.md: added GBRAIN_CONTEXT_LOAD and GBRAIN_SAVE_RESULTS
to resolver table.

CHANGELOG.md: expanded v0.18.0.0 entry with GBrain v0.10.0 integration
details (triggers, expanded brain-awareness, DX improvements, Hermes
brain support), updated date.

CLAUDE.md: added gbrain to resolvers/ directory comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: routing E2E stops writing to user's ~/.claude/skills/

installSkills() was copying SKILL.md files to both project-level
(.claude/skills/ in tmpDir) and user-level (~/.claude/skills/).
Writing to the user's real install fails when symlinks point to
different worktrees or dangling targets (ENOENT on copyFileSync).

Now installs to project-level only. The test already sets cwd to
the tmpDir, so project-level discovery works.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* chore: scale Gemini E2E back to smoke test

Gemini CLI gets lost in worktrees on complex tasks (review times out
at 600s, discover-skill hits exit 124). Nobody uses Gemini for gstack
skill execution. Replace the two failing tests (gemini-discover-skill
and gemini-review-findings) with a single smoke test that verifies
Gemini can start and read the README. 90s timeout, no skill invocation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 10:41:38 -07:00

524 lines
18 KiB
TypeScript

/**
* Host config system tests — 100% coverage of host-config.ts, hosts/index.ts,
* host-config-export.ts, and golden-file regression checks.
*/
import { describe, test, expect } from 'bun:test';
import * as fs from 'fs';
import * as path from 'path';
import { validateHostConfig, validateAllConfigs, type HostConfig } from '../scripts/host-config';
import {
ALL_HOST_CONFIGS,
ALL_HOST_NAMES,
HOST_CONFIG_MAP,
getHostConfig,
resolveHostArg,
getExternalHosts,
claude,
codex,
factory,
kiro,
opencode,
slate,
cursor,
openclaw,
} from '../hosts/index';
import { HOST_PATHS } from '../scripts/resolvers/types';
const ROOT = path.resolve(import.meta.dir, '..');
// ─── hosts/index.ts ─────────────────────────────────────────
describe('hosts/index.ts', () => {
test('ALL_HOST_CONFIGS has 10 hosts', () => {
expect(ALL_HOST_CONFIGS.length).toBe(10);
});
test('ALL_HOST_NAMES matches config names', () => {
expect(ALL_HOST_NAMES).toEqual(ALL_HOST_CONFIGS.map(c => c.name));
});
test('HOST_CONFIG_MAP keys match names', () => {
for (const config of ALL_HOST_CONFIGS) {
expect(HOST_CONFIG_MAP[config.name]).toBe(config);
}
});
test('individual config re-exports match registry', () => {
expect(claude.name).toBe('claude');
expect(codex.name).toBe('codex');
expect(factory.name).toBe('factory');
expect(kiro.name).toBe('kiro');
expect(opencode.name).toBe('opencode');
expect(slate.name).toBe('slate');
expect(cursor.name).toBe('cursor');
expect(openclaw.name).toBe('openclaw');
});
test('getHostConfig returns correct config', () => {
const c = getHostConfig('codex');
expect(c.name).toBe('codex');
expect(c.displayName).toBe('OpenAI Codex CLI');
});
test('getHostConfig throws on unknown host', () => {
expect(() => getHostConfig('nonexistent')).toThrow('Unknown host');
});
test('resolveHostArg resolves direct names', () => {
for (const name of ALL_HOST_NAMES) {
expect(resolveHostArg(name)).toBe(name);
}
});
test('resolveHostArg resolves aliases', () => {
expect(resolveHostArg('agents')).toBe('codex');
expect(resolveHostArg('droid')).toBe('factory');
});
test('resolveHostArg throws on unknown alias', () => {
expect(() => resolveHostArg('nonexistent')).toThrow('Unknown host');
});
test('getExternalHosts excludes claude', () => {
const external = getExternalHosts();
expect(external.find(c => c.name === 'claude')).toBeUndefined();
expect(external.length).toBe(ALL_HOST_CONFIGS.length - 1);
});
test('every host has a unique name', () => {
const names = new Set(ALL_HOST_NAMES);
expect(names.size).toBe(ALL_HOST_NAMES.length);
});
test('every host has a unique hostSubdir', () => {
const subdirs = new Set(ALL_HOST_CONFIGS.map(c => c.hostSubdir));
expect(subdirs.size).toBe(ALL_HOST_CONFIGS.length);
});
test('every host has a unique globalRoot', () => {
const roots = new Set(ALL_HOST_CONFIGS.map(c => c.globalRoot));
expect(roots.size).toBe(ALL_HOST_CONFIGS.length);
});
});
// ─── validateHostConfig ─────────────────────────────────────
describe('validateHostConfig', () => {
function makeValid(): HostConfig {
return {
name: 'test-host',
displayName: 'Test Host',
cliCommand: 'testcli',
globalRoot: '.test/skills/gstack',
localSkillRoot: '.test/skills/gstack',
hostSubdir: '.test',
usesEnvVars: true,
frontmatter: { mode: 'allowlist', keepFields: ['name', 'description'] },
generation: { generateMetadata: false },
pathRewrites: [],
runtimeRoot: { globalSymlinks: ['bin'] },
install: { prefixable: false, linkingStrategy: 'symlink-generated' },
};
}
test('valid config passes', () => {
expect(validateHostConfig(makeValid())).toEqual([]);
});
test('invalid name is caught', () => {
const c = makeValid();
c.name = 'UPPER_CASE';
const errors = validateHostConfig(c);
expect(errors.some(e => e.includes('name'))).toBe(true);
});
test('name with special chars is caught', () => {
const c = makeValid();
c.name = 'has spaces';
expect(validateHostConfig(c).length).toBeGreaterThan(0);
});
test('empty displayName is caught', () => {
const c = makeValid();
c.displayName = '';
expect(validateHostConfig(c).some(e => e.includes('displayName'))).toBe(true);
});
test('invalid cliCommand is caught', () => {
const c = makeValid();
c.cliCommand = 'has spaces';
expect(validateHostConfig(c).some(e => e.includes('cliCommand'))).toBe(true);
});
test('invalid cliAlias is caught', () => {
const c = makeValid();
c.cliAliases = ['good', 'BAD!'];
expect(validateHostConfig(c).some(e => e.includes('cliAlias'))).toBe(true);
});
test('valid cliAliases pass', () => {
const c = makeValid();
c.cliAliases = ['alias-one', 'alias-two'];
expect(validateHostConfig(c)).toEqual([]);
});
test('invalid globalRoot is caught', () => {
const c = makeValid();
c.globalRoot = 'path with spaces';
expect(validateHostConfig(c).some(e => e.includes('globalRoot'))).toBe(true);
});
test('invalid localSkillRoot is caught', () => {
const c = makeValid();
c.localSkillRoot = 'invalid<path>';
expect(validateHostConfig(c).some(e => e.includes('localSkillRoot'))).toBe(true);
});
test('invalid hostSubdir is caught', () => {
const c = makeValid();
c.hostSubdir = 'no spaces allowed';
expect(validateHostConfig(c).some(e => e.includes('hostSubdir'))).toBe(true);
});
test('invalid frontmatter.mode is caught', () => {
const c = makeValid();
(c.frontmatter as any).mode = 'invalid';
expect(validateHostConfig(c).some(e => e.includes('frontmatter.mode'))).toBe(true);
});
test('invalid linkingStrategy is caught', () => {
const c = makeValid();
(c.install as any).linkingStrategy = 'invalid';
expect(validateHostConfig(c).some(e => e.includes('linkingStrategy'))).toBe(true);
});
test('paths with $ and ~ are valid', () => {
const c = makeValid();
c.globalRoot = '$HOME/.test/skills/gstack';
c.localSkillRoot = '~/.test/skills/gstack';
expect(validateHostConfig(c)).toEqual([]);
});
test('shell injection attempt in cliCommand is caught', () => {
const c = makeValid();
c.cliCommand = 'opencode;rm -rf /';
expect(validateHostConfig(c).some(e => e.includes('cliCommand'))).toBe(true);
});
});
// ─── validateAllConfigs ─────────────────────────────────────
describe('validateAllConfigs', () => {
test('real configs all pass validation', () => {
const errors = validateAllConfigs(ALL_HOST_CONFIGS);
expect(errors).toEqual([]);
});
test('duplicate name detected', () => {
const dup = { ...codex, name: 'claude' } as HostConfig;
const errors = validateAllConfigs([claude, dup]);
expect(errors.some(e => e.includes('Duplicate name'))).toBe(true);
});
test('duplicate hostSubdir detected', () => {
const dup = { ...codex, name: 'dup-host', hostSubdir: '.claude', globalRoot: '.dup/skills/gstack' } as HostConfig;
const errors = validateAllConfigs([claude, dup]);
expect(errors.some(e => e.includes('Duplicate hostSubdir'))).toBe(true);
});
test('duplicate globalRoot detected', () => {
const dup = { ...codex, name: 'dup-host', hostSubdir: '.dup', globalRoot: '.claude/skills/gstack' } as HostConfig;
const errors = validateAllConfigs([claude, dup]);
expect(errors.some(e => e.includes('Duplicate globalRoot'))).toBe(true);
});
test('per-config validation errors are prefixed with host name', () => {
const bad = { ...codex, name: 'BAD', cliCommand: 'also bad' } as HostConfig;
const errors = validateAllConfigs([bad]);
expect(errors.every(e => e.startsWith('[BAD]'))).toBe(true);
});
});
// ─── HOST_PATHS derivation ──────────────────────────────────
describe('HOST_PATHS derivation from configs', () => {
test('Claude uses literal home paths (no env vars)', () => {
expect(HOST_PATHS.claude.skillRoot).toBe('~/.claude/skills/gstack');
expect(HOST_PATHS.claude.binDir).toBe('~/.claude/skills/gstack/bin');
expect(HOST_PATHS.claude.browseDir).toBe('~/.claude/skills/gstack/browse/dist');
expect(HOST_PATHS.claude.designDir).toBe('~/.claude/skills/gstack/design/dist');
});
test('Codex uses $GSTACK_ROOT env vars', () => {
expect(HOST_PATHS.codex.skillRoot).toBe('$GSTACK_ROOT');
expect(HOST_PATHS.codex.binDir).toBe('$GSTACK_BIN');
expect(HOST_PATHS.codex.browseDir).toBe('$GSTACK_BROWSE');
expect(HOST_PATHS.codex.designDir).toBe('$GSTACK_DESIGN');
});
test('every host with usesEnvVars=true gets env var paths', () => {
for (const config of ALL_HOST_CONFIGS) {
if (config.usesEnvVars) {
expect(HOST_PATHS[config.name].skillRoot).toBe('$GSTACK_ROOT');
expect(HOST_PATHS[config.name].binDir).toBe('$GSTACK_BIN');
}
}
});
test('every host with usesEnvVars=false gets literal paths', () => {
for (const config of ALL_HOST_CONFIGS) {
if (!config.usesEnvVars) {
expect(HOST_PATHS[config.name].skillRoot).toContain('~/');
expect(HOST_PATHS[config.name].binDir).toContain('/bin');
}
}
});
test('localSkillRoot matches config for every host', () => {
for (const config of ALL_HOST_CONFIGS) {
expect(HOST_PATHS[config.name].localSkillRoot).toBe(config.localSkillRoot);
}
});
test('HOST_PATHS has entry for every registered host', () => {
for (const name of ALL_HOST_NAMES) {
expect(HOST_PATHS[name]).toBeDefined();
}
});
});
// ─── host-config-export.ts CLI ──────────────────────────────
describe('host-config-export.ts CLI', () => {
const EXPORT_SCRIPT = path.join(ROOT, 'scripts', 'host-config-export.ts');
function run(...args: string[]): { stdout: string; stderr: string; exitCode: number } {
const result = Bun.spawnSync(['bun', 'run', EXPORT_SCRIPT, ...args], {
cwd: ROOT, stdout: 'pipe', stderr: 'pipe',
});
return {
stdout: result.stdout.toString().trim(),
stderr: result.stderr.toString().trim(),
exitCode: result.exitCode,
};
}
test('list prints all host names', () => {
const { stdout, exitCode } = run('list');
expect(exitCode).toBe(0);
const names = stdout.split('\n');
expect(names).toEqual(ALL_HOST_NAMES);
});
test('get returns string field', () => {
const { stdout, exitCode } = run('get', 'codex', 'globalRoot');
expect(exitCode).toBe(0);
expect(stdout).toBe('.codex/skills/gstack');
});
test('get returns boolean as 1/0', () => {
const { stdout: t } = run('get', 'claude', 'usesEnvVars');
expect(t).toBe('0');
const { stdout: f } = run('get', 'codex', 'usesEnvVars');
expect(f).toBe('1');
});
test('get with missing args exits 1', () => {
const { exitCode } = run('get', 'codex');
expect(exitCode).toBe(1);
});
test('get with unknown field exits 1', () => {
const { exitCode } = run('get', 'codex', 'nonexistent');
expect(exitCode).toBe(1);
});
test('get with unknown host exits 1', () => {
const { exitCode } = run('get', 'nonexistent', 'name');
expect(exitCode).not.toBe(0);
});
test('validate passes for real configs', () => {
const { stdout, exitCode } = run('validate');
expect(exitCode).toBe(0);
expect(stdout).toContain('configs valid');
});
test('symlinks returns asset list', () => {
const { stdout, exitCode } = run('symlinks', 'codex');
expect(exitCode).toBe(0);
const lines = stdout.split('\n');
expect(lines).toContain('bin');
expect(lines).toContain('ETHOS.md');
expect(lines).toContain('review/checklist.md');
});
test('symlinks with missing host exits 1', () => {
const { exitCode } = run('symlinks');
expect(exitCode).toBe(1);
});
test('detect finds claude (since we are running in claude)', () => {
const { stdout, exitCode } = run('detect');
expect(exitCode).toBe(0);
// claude binary should be on PATH in this environment
expect(stdout).toContain('claude');
});
test('unknown command exits 1', () => {
const { exitCode } = run('badcommand');
expect(exitCode).toBe(1);
});
});
// ─── Golden-file regression ─────────────────────────────────
describe('golden-file regression', () => {
const GOLDEN_DIR = path.join(ROOT, 'test', 'fixtures', 'golden');
test('Claude ship skill matches golden baseline', () => {
const golden = fs.readFileSync(path.join(GOLDEN_DIR, 'claude-ship-SKILL.md'), 'utf-8');
const current = fs.readFileSync(path.join(ROOT, 'ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
test('Codex ship skill matches golden baseline', () => {
const golden = fs.readFileSync(path.join(GOLDEN_DIR, 'codex-ship-SKILL.md'), 'utf-8');
const current = fs.readFileSync(path.join(ROOT, '.agents', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
test('Factory ship skill matches golden baseline', () => {
const golden = fs.readFileSync(path.join(GOLDEN_DIR, 'factory-ship-SKILL.md'), 'utf-8');
const current = fs.readFileSync(path.join(ROOT, '.factory', 'skills', 'gstack-ship', 'SKILL.md'), 'utf-8');
expect(current).toBe(golden);
});
});
// ─── Individual host config correctness ─────────────────────
describe('host config correctness', () => {
test('claude is the only prefixable host', () => {
for (const config of ALL_HOST_CONFIGS) {
if (config.name === 'claude') {
expect(config.install.prefixable).toBe(true);
} else {
expect(config.install.prefixable).toBe(false);
}
}
});
test('claude is the only host with real-dir-symlink strategy', () => {
for (const config of ALL_HOST_CONFIGS) {
if (config.name === 'claude') {
expect(config.install.linkingStrategy).toBe('real-dir-symlink');
} else {
expect(config.install.linkingStrategy).toBe('symlink-generated');
}
}
});
test('claude does not use env vars', () => {
expect(claude.usesEnvVars).toBe(false);
});
test('all external hosts use env vars', () => {
for (const config of getExternalHosts()) {
expect(config.usesEnvVars).toBe(true);
}
});
test('codex has 1024-char description limit with error behavior', () => {
expect(codex.frontmatter.descriptionLimit).toBe(1024);
expect(codex.frontmatter.descriptionLimitBehavior).toBe('error');
});
test('codex generates openai.yaml metadata', () => {
expect(codex.generation.generateMetadata).toBe(true);
expect(codex.generation.metadataFormat).toBe('openai.yaml');
});
test('codex has sidecar config', () => {
expect(codex.sidecar).toBeDefined();
expect(codex.sidecar!.path).toBe('.agents/skills/gstack');
});
test('factory has tool rewrites', () => {
expect(factory.toolRewrites).toBeDefined();
expect(Object.keys(factory.toolRewrites!).length).toBeGreaterThan(0);
expect(factory.toolRewrites!['use the Bash tool']).toBe('run this command');
});
test('factory has conditional disable-model-invocation field', () => {
expect(factory.frontmatter.conditionalFields).toBeDefined();
expect(factory.frontmatter.conditionalFields!.length).toBe(1);
expect(factory.frontmatter.conditionalFields![0].if).toEqual({ sensitive: true });
expect(factory.frontmatter.conditionalFields![0].add).toEqual({ 'disable-model-invocation': true });
});
test('codex has suppressedResolvers for self-invocation prevention', () => {
expect(codex.suppressedResolvers).toBeDefined();
expect(codex.suppressedResolvers).toContain('CODEX_SECOND_OPINION');
expect(codex.suppressedResolvers).toContain('ADVERSARIAL_STEP');
expect(codex.suppressedResolvers).toContain('REVIEW_ARMY');
});
test('codex has boundary instruction', () => {
expect(codex.boundaryInstruction).toBeDefined();
expect(codex.boundaryInstruction).toContain('Do NOT read');
});
test('openclaw has tool rewrites for exec/read/write', () => {
expect(openclaw.toolRewrites).toBeDefined();
expect(openclaw.toolRewrites!['use the Bash tool']).toBe('use the exec tool');
expect(openclaw.toolRewrites!['use the Read tool']).toBe('use the read tool');
});
test('openclaw has CLAUDE.md→AGENTS.md path rewrite', () => {
expect(openclaw.pathRewrites.some(r => r.from === 'CLAUDE.md' && r.to === 'AGENTS.md')).toBe(true);
});
test('openclaw has no adapter (dead code removed)', () => {
expect(openclaw.adapter).toBeUndefined();
});
test('openclaw has no staticFiles (SOUL.md removed)', () => {
expect(openclaw.staticFiles).toBeUndefined();
});
test('openclaw includeSkills is empty (native skills replaced generated ones)', () => {
expect(openclaw.generation.includeSkills).toBeDefined();
expect(openclaw.generation.includeSkills!.length).toBe(0);
});
test('every host has coAuthorTrailer or undefined', () => {
// Claude, Codex, Factory, OpenClaw have explicit trailers
expect(claude.coAuthorTrailer).toContain('Claude');
expect(codex.coAuthorTrailer).toContain('Codex');
expect(factory.coAuthorTrailer).toContain('Factory');
expect(openclaw.coAuthorTrailer).toContain('OpenClaw');
});
test('every external host skips the codex skill', () => {
for (const config of getExternalHosts()) {
expect(config.generation.skipSkills).toContain('codex');
}
});
test('every host has at least one pathRewrite (except claude)', () => {
for (const config of getExternalHosts()) {
expect(config.pathRewrites.length).toBeGreaterThan(0);
}
expect(claude.pathRewrites.length).toBe(0);
});
test('every host has runtimeRoot.globalSymlinks', () => {
for (const config of ALL_HOST_CONFIGS) {
expect(config.runtimeRoot.globalSymlinks.length).toBeGreaterThan(0);
expect(config.runtimeRoot.globalSymlinks).toContain('bin');
expect(config.runtimeRoot.globalSymlinks).toContain('ETHOS.md');
}
});
});