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:
parent
2325c90abb
commit
0fc748d2d4
2 changed files with 165 additions and 1 deletions
134
server/src/__tests__/29-default-provider.test.ts
Normal file
134
server/src/__tests__/29-default-provider.test.ts
Normal 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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -94,9 +94,39 @@ export function OnboardingWizard() {
|
||||||
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
queryClient.invalidateQueries({ queryKey: queryKeys.companies.all });
|
||||||
|
|
||||||
// [nexus] hermes_local doesn't require a cwd; directory is optional
|
// [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() } : {})
|
? (rootDir.trim() ? { cwd: 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 = {
|
const runtimeConfig = {
|
||||||
heartbeat: {
|
heartbeat: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue