fix(FN-2055): harden node test utilities and import resolution

- Convert seed-sample-nodes into a test-only helper and remove direct execution against the real central database
- Replace private-looking host examples with RFC 5737 test-net addresses in node connection docs and ConnectNodeModal expectations
- Add Vitest aliases for @fusion/core and @fusion/engine to stabilize cross-package test imports
- Load AgentReflectionService via local engine source path in in-process runtime to avoid alias resolution failures
This commit is contained in:
Fusion
2026-04-18 12:21:14 -07:00
committed by gsxdsm
parent eccaed9e69
commit 7affac3d4b
7 changed files with 31 additions and 84 deletions

View File

@@ -1,22 +1,17 @@
/**
* Seed script for populating the central database with sample nodes.
* Test utility for populating a CentralCore instance with sample nodes.
*
* This script creates a realistic set of nodes (1 local + 5 remote) for
* visual testing of the multi-node dashboard.
* Used exclusively in tests — creates 1 local + 5 remote nodes
* for testing the multi-node dashboard. Must only be called with
* test-scoped CentralCore instances (temp directories).
*
* Usage:
* - Direct execution (seeds real central database):
* npx tsx packages/core/src/__tests__/seed-sample-nodes.ts
*
* - As a module (for tests):
* import { seedSampleNodes } from "./seed-sample-nodes";
* await seedSampleNodes(central);
* Usage (tests only):
* import { seedSampleNodes } from "./seed-sample-nodes";
* await seedSampleNodes(central); // central backed by temp directory
*/
import { CentralCore } from "../central-core.js";
import type { NodeConfig, NodeStatus } from "../types.js";
import { homedir } from "node:os";
import { join } from "node:path";
/** Sample remote nodes to create for visual testing */
const SAMPLE_REMOTE_NODES = [
@@ -112,59 +107,3 @@ export async function seedSampleNodes(central: CentralCore): Promise<NodeConfig[
return nodes;
}
/**
* Seed the real central database and print results.
* Use this when running directly via tsx.
*/
async function main(): Promise<void> {
console.log("\n🌐 Seeding sample nodes into central database...\n");
const central = new CentralCore();
await central.init();
try {
const nodes = await seedSampleNodes(central);
console.log("\n📊 Registered nodes:\n");
console.log("┌─────────────────────────────────────────┬────────┬──────────────────────────────┬─────────┐");
console.log("│ Name │ Type │ URL │ Status │");
console.log("├─────────────────────────────────────────┼────────┼──────────────────────────────┼─────────┤");
for (const node of nodes) {
const type = node.type.padEnd(6);
const name = node.name.slice(0, 39).padEnd(39);
const url = (node.url ?? "-").slice(0, 28).padEnd(28);
const status = node.status.padEnd(7);
console.log(`${name}${type}${url}${status}`);
}
console.log("└─────────────────────────────────────────┴────────┴──────────────────────────────┴─────────┘");
// Summary stats
const total = nodes.length;
const localCount = nodes.filter((n) => n.type === "local").length;
const remoteCount = nodes.filter((n) => n.type === "remote").length;
const onlineCount = nodes.filter((n) => n.status === "online").length;
const offlineCount = nodes.filter((n) => n.status === "offline").length;
const errorCount = nodes.filter((n) => n.status === "error").length;
const connectingCount = nodes.filter((n) => n.status === "connecting").length;
console.log("\n📈 Summary:");
console.log(` Total nodes: ${total}`);
console.log(` Local: ${localCount}, Remote: ${remoteCount}`);
console.log(` Online: ${onlineCount}, Offline: ${offlineCount}, Error: ${errorCount}, Connecting: ${connectingCount}`);
console.log(`\n✅ Database: ${central.getDatabasePath()}\n`);
} finally {
await central.close();
}
}
// Run if executed directly
const isMainModule = process.argv[1]?.endsWith("seed-sample-nodes.ts");
if (isMainModule) {
main().catch((err) => {
console.error("\n❌ Seeding failed:", err);
process.exit(1);
});
}

View File

@@ -15,7 +15,7 @@ export type ConnectionErrorType =
export interface ConnectionResult {
/** Whether the connection test succeeded */
success: boolean;
/** Normalized URL (e.g., "http://192.168.1.100:3000") */
/** Normalized URL (e.g., "http://192.0.2.10:3000") */
url: string;
/** Latency in milliseconds for the successful health check */
latencyMs?: number;
@@ -40,7 +40,7 @@ export interface ConnectionResult {
}
export interface ConnectionOptions {
/** IP address or hostname (e.g., "192.168.1.100" or "my-server.local") */
/** IP address or hostname (e.g., "192.0.2.10" or "my-server.local") */
host: string;
/** Port number (1-65535) */
port: number;

View File

@@ -198,7 +198,7 @@ export function ConnectNodeModal({ open, onClose, onConnected, addToast, onSubmi
type="text"
value={host}
onChange={(event) => setHost(event.target.value)}
placeholder="192.168.1.100 or my-server.local"
placeholder="192.0.2.10 or my-server.local"
disabled={isSubmitting}
aria-invalid={Boolean(errors.host)}
/>

View File

@@ -44,7 +44,7 @@ describe("ConnectNodeModal", () => {
expect(screen.getByLabelText("Connect to Node")).toBeInTheDocument();
expect(screen.getByPlaceholderText("Build Server")).toBeInTheDocument();
expect(screen.getByPlaceholderText("192.168.1.100 or my-server.local")).toBeInTheDocument();
expect(screen.getByPlaceholderText("192.0.2.10 or my-server.local")).toBeInTheDocument();
});
it("does not render when closed", () => {
@@ -57,7 +57,7 @@ describe("ConnectNodeModal", () => {
render(<ConnectNodeModal {...defaultProps} />);
// Fill in host but not name
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});
@@ -72,7 +72,7 @@ describe("ConnectNodeModal", () => {
render(<ConnectNodeModal {...defaultProps} />);
// Get the host input directly
const hostInput = screen.getByPlaceholderText("192.168.1.100 or my-server.local");
const hostInput = screen.getByPlaceholderText("192.0.2.10 or my-server.local");
expect(hostInput).toBeInTheDocument();
// Host should be empty initially
@@ -85,7 +85,7 @@ describe("ConnectNodeModal", () => {
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});
@@ -103,7 +103,7 @@ describe("ConnectNodeModal", () => {
it("shows URL preview as host and port are filled", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});
@@ -113,7 +113,7 @@ describe("ConnectNodeModal", () => {
it("updates URL preview when port changes", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "my-server.local" },
});
@@ -127,7 +127,7 @@ describe("ConnectNodeModal", () => {
it("strips protocol from host in URL preview", () => {
render(<ConnectNodeModal {...defaultProps} />);
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "https://my-server.local" },
});
@@ -146,7 +146,7 @@ describe("ConnectNodeModal", () => {
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});
@@ -171,7 +171,7 @@ describe("ConnectNodeModal", () => {
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "invalid-host" },
});
@@ -189,7 +189,7 @@ describe("ConnectNodeModal", () => {
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Test Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});
@@ -226,7 +226,7 @@ describe("ConnectNodeModal", () => {
fireEvent.change(screen.getByPlaceholderText("Build Server"), {
target: { value: "Custom Node" },
});
fireEvent.change(screen.getByPlaceholderText("192.168.1.100 or my-server.local"), {
fireEvent.change(screen.getByPlaceholderText("192.0.2.10 or my-server.local"), {
target: { value: "192.168.1.100" },
});

View File

@@ -11,6 +11,7 @@ export default defineConfig({
resolve: {
alias: {
"@fusion/core": resolve(__dirname, "../core/src/index.ts"),
"@fusion/engine": resolve(__dirname, "../engine/src/index.ts"),
},
},
test: {

View File

@@ -292,10 +292,10 @@ export class InProcessRuntime
}
// 5c. Initialize AgentReflectionService (requires agentStore and reflectionStore)
let reflectionService: import("@fusion/engine").AgentReflectionService | undefined;
let reflectionService: import("../agent-reflection.js").AgentReflectionService | undefined;
if (agentStoreForReflection && reflectionStoreForService) {
try {
const { AgentReflectionService: AgentReflectionServiceClass } = await import("@fusion/engine");
const { AgentReflectionService: AgentReflectionServiceClass } = await import("../agent-reflection.js");
reflectionService = new AgentReflectionServiceClass({
agentStore: agentStoreForReflection,
taskStore: this.taskStore,

View File

@@ -1,10 +1,17 @@
import { defineConfig } from "vitest/config";
import { availableParallelism } from "node:os";
import { fileURLToPath } from "node:url";
const defaultMaxWorkers = Math.max(1, Math.min(2, Math.ceil(availableParallelism() / 8)));
const maxWorkers = Number.parseInt(process.env.VITEST_MAX_WORKERS ?? String(defaultMaxWorkers), 10);
const coreSourceEntry = fileURLToPath(new URL("../core/src/index.ts", import.meta.url));
export default defineConfig({
resolve: {
alias: {
"@fusion/core": coreSourceEntry,
},
},
test: {
include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
passWithNoTests: true,