feat(29-02): Hermes skill injection + default provider integration tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nexus Dev 2026-04-02 17:43:47 +00:00
parent 2325c90abb
commit 0fc748d2d4
2 changed files with 165 additions and 1 deletions

View file

@ -0,0 +1,134 @@
// Integration tests for Phase 29: default-provider
// Covers: adapter probe logic, Hermes promptTemplate contract, default agent instructions bundle
import { describe, it, expect } from "vitest";
import { findServerAdapter } from "../adapters/index.js";
import {
loadDefaultAgentInstructionsBundle,
resolveDefaultAgentInstructionsBundleRole,
} from "../services/default-agent-instructions.js";
// ---------------------------------------------------------------------------
// The same hermesPromptTemplate constructed in NexusOnboardingWizard.
// Duplicated here intentionally — this test validates the *contract* (that all
// required Mustache variables are present), not the wizard implementation.
// ---------------------------------------------------------------------------
const APP_NAME = "Nexus"; // VOCAB.appName value
const hermesPromptTemplate = [
`You are "{{agentName}}", an AI agent managed by ${APP_NAME}.`,
"",
"Your identity:",
" Agent ID: {{agentId}}",
" Company ID: {{companyId}}",
" API Base: {{paperclipApiUrl}}",
" Run ID: {{runId}}",
"",
"IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.",
'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.',
"",
"Before starting any task:",
"1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions",
"2. Follow the HEARTBEAT.md workflow from your instructions",
"3. Use TOOLS.md for available API endpoints",
"",
"{{#taskId}}",
"Assigned task: {{taskId}} - {{taskTitle}}",
"{{/taskId}}",
].join("\n");
// ---------------------------------------------------------------------------
// Test group 1: Adapter probe route logic
// ---------------------------------------------------------------------------
describe("adapter probe route logic", () => {
it("findServerAdapter returns hermes_local adapter with testEnvironment", () => {
const adapter = findServerAdapter("hermes_local");
expect(adapter).not.toBeNull();
expect(adapter?.type).toBe("hermes_local");
expect(typeof adapter?.testEnvironment).toBe("function");
});
it("hermes testEnvironment handles missing CLI gracefully", async () => {
const adapter = findServerAdapter("hermes_local");
expect(adapter?.testEnvironment).toBeDefined();
const result = await adapter!.testEnvironment!({
companyId: "",
adapterType: "hermes_local",
config: {},
});
// Result should always have a status and checks array
expect(typeof result.status).toBe("string");
expect(Array.isArray(result.checks)).toBe(true);
// If hermes CLI is not installed in CI, there should be an error check
// with a code containing "not_found" or "cli".
// If it IS installed, the checks should pass — either is acceptable.
const hasCliError = result.checks.some(
(c: { level: string; code?: string }) =>
c.level === "error" &&
(c.code?.includes("not_found") || c.code?.includes("cli"))
);
const isAvailable = result.status === "ok" || result.status === "pass";
// Either CLI is available (no error checks) or not available (has error checks)
expect(hasCliError || isAvailable).toBe(true);
});
});
// ---------------------------------------------------------------------------
// Test group 2: Hermes promptTemplate construction
// ---------------------------------------------------------------------------
describe("hermes promptTemplate construction", () => {
it("hermes promptTemplate contains required Mustache variables", () => {
expect(hermesPromptTemplate).toContain("{{agentName}}");
expect(hermesPromptTemplate).toContain("{{agentId}}");
expect(hermesPromptTemplate).toContain("{{companyId}}");
expect(hermesPromptTemplate).toContain("{{paperclipApiUrl}}");
expect(hermesPromptTemplate).toContain("{{runId}}");
expect(hermesPromptTemplate).toContain("{{taskId}}");
expect(hermesPromptTemplate).toContain("{{taskTitle}}");
});
it("hermes promptTemplate instructs agent to call /api/agents/me", () => {
expect(hermesPromptTemplate).toContain("agents/me");
});
it("hermes promptTemplate mentions HEARTBEAT.md workflow", () => {
expect(hermesPromptTemplate).toContain("HEARTBEAT.md");
});
});
// ---------------------------------------------------------------------------
// Test group 3: Default agent instructions bundle (DFLT-03)
// ---------------------------------------------------------------------------
describe("default agent instructions bundle", () => {
it("loadDefaultAgentInstructionsBundle loads ceo bundle with all 4 files", async () => {
const bundle = await loadDefaultAgentInstructionsBundle("ceo");
expect(Object.keys(bundle)).toContain("AGENTS.md");
expect(Object.keys(bundle)).toContain("HEARTBEAT.md");
expect(Object.keys(bundle)).toContain("SOUL.md");
expect(Object.keys(bundle)).toContain("TOOLS.md");
expect(bundle["HEARTBEAT.md"].length).toBeGreaterThan(0);
});
it("loadDefaultAgentInstructionsBundle loads engineer bundle with all 4 files", async () => {
const bundle = await loadDefaultAgentInstructionsBundle("engineer");
expect(Object.keys(bundle)).toContain("AGENTS.md");
expect(Object.keys(bundle)).toContain("HEARTBEAT.md");
expect(Object.keys(bundle)).toContain("SOUL.md");
expect(Object.keys(bundle)).toContain("TOOLS.md");
expect(bundle["HEARTBEAT.md"].length).toBeGreaterThan(0);
});
it("resolveDefaultAgentInstructionsBundleRole maps known roles correctly", () => {
expect(resolveDefaultAgentInstructionsBundleRole("ceo")).toBe("ceo");
expect(resolveDefaultAgentInstructionsBundleRole("engineer")).toBe("engineer");
expect(resolveDefaultAgentInstructionsBundleRole("general")).toBe("general");
expect(resolveDefaultAgentInstructionsBundleRole("unknown-role")).toBe("default");
expect(resolveDefaultAgentInstructionsBundleRole("")).toBe("default");
});
});

View file

@ -94,9 +94,39 @@ export function OnboardingWizard() {
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
// [nexus] hermes_local doesn't require a cwd; directory is optional
const adapterConfig = defaultAdapter === "hermes_local"
const baseAdapterConfig = defaultAdapter === "hermes_local"
? (rootDir.trim() ? { cwd: rootDir.trim() } : {})
: { cwd: rootDir.trim() };
// [nexus] Hermes agents need a promptTemplate so they follow the Nexus heartbeat
// workflow. The server's ensureDefaultInstructionsBundle will materialize this as
// AGENTS.md alongside the full role bundle (HEARTBEAT.md, SOUL.md, TOOLS.md).
const hermesPromptTemplate = [
`You are "{{agentName}}", an AI agent managed by ${VOCAB.appName}.`,
"",
"Your identity:",
" Agent ID: {{agentId}}",
" Company ID: {{companyId}}",
" API Base: {{paperclipApiUrl}}",
" Run ID: {{runId}}",
"",
"IMPORTANT: Use the `terminal` tool with `curl` for ALL API calls.",
'IMPORTANT: Always include `-H "X-Paperclip-Run-Id: {{runId}}"` on API calls that modify data.',
"",
"Before starting any task:",
"1. Call `GET {{paperclipApiUrl}}/api/agents/me` to retrieve your managed instructions",
"2. Follow the HEARTBEAT.md workflow from your instructions",
"3. Use TOOLS.md for available API endpoints",
"",
"{{#taskId}}",
"Assigned task: {{taskId}} - {{taskTitle}}",
"{{/taskId}}",
].join("\n");
const adapterConfig = defaultAdapter === "hermes_local"
? { ...baseAdapterConfig, promptTemplate: hermesPromptTemplate, persistSession: true }
: baseAdapterConfig;
const runtimeConfig = {
heartbeat: {
enabled: true,