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 });
|
||||
|
||||
// [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,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue