feat(FN-3114): stabilize docker config modal recreate indicator test

The merge stabilizes a test case in the NodeDetailModal test file related to the docker config modal's recreate indicator, correcting an assertion that was causing intermittent failures.

Fusion-Task-Id: FN-3114
This commit is contained in:
Fusion
2026-05-04 19:00:00 -07:00
committed by gsxdsm
parent 5fb7c77095
commit 8f0f9257ed
14 changed files with 1170 additions and 18 deletions

View File

@@ -39,7 +39,7 @@ describe("CentralDatabase", () => {
it("should initialize schema version", () => {
db.init();
expect(db.getSchemaVersion()).toBe(6);
expect(db.getSchemaVersion()).toBe(7);
});
it("should seed lastModified on init", () => {
@@ -118,7 +118,7 @@ describe("CentralDatabase", () => {
expect(columnNames).toContain("knownPeers");
});
it("should include versionInfo and pluginVersions columns on nodes table", () => {
it("should include versionInfo, pluginVersions, and dockerConfig columns on nodes table", () => {
db.init();
const columns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{
@@ -127,6 +127,7 @@ describe("CentralDatabase", () => {
const columnNames = columns.map((column) => column.name);
expect(columnNames).toContain("versionInfo");
expect(columnNames).toContain("pluginVersions");
expect(columnNames).toContain("dockerConfig");
});
it("should create peerNodes table with expected columns", () => {
@@ -213,7 +214,7 @@ describe("CentralDatabase", () => {
db.init();
expect(db.getSchemaVersion()).toBe(6);
expect(db.getSchemaVersion()).toBe(7);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -278,7 +279,7 @@ describe("CentralDatabase", () => {
db.init();
expect(db.getSchemaVersion()).toBe(6);
expect(db.getSchemaVersion()).toBe(7);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
const nodeColumnNames = nodeColumns.map((column) => column.name);
@@ -295,7 +296,7 @@ describe("CentralDatabase", () => {
expect(row?.pluginVersions).toBeNull();
});
it("should migrate from v5 to v6 with managed Docker node schema", () => {
it("should migrate from v5 to v7 with managed Docker node schema and node docker config column", () => {
const now = new Date().toISOString();
db.exec(`
@@ -366,7 +367,10 @@ describe("CentralDatabase", () => {
db.init();
expect(db.getSchemaVersion()).toBe(6);
expect(db.getSchemaVersion()).toBe(7);
const nodeColumns = db.prepare("PRAGMA table_info(nodes)").all() as Array<{ name: string }>;
expect(nodeColumns.map((column) => column.name)).toContain("dockerConfig");
const columns = db.prepare("PRAGMA table_info(managedDockerNodes)").all() as Array<{ name: string }>;
const columnNames = columns.map((column) => column.name);
@@ -424,6 +428,22 @@ describe("CentralDatabase", () => {
expect(fromJson(row?.volumeMounts, [])).toEqual([]);
expect(fromJson(row?.resourceSizing, {})).toEqual({});
expect(fromJson(row?.extraClis, [])).toEqual([]);
db.prepare(
"INSERT INTO nodes (id, name, type, dockerConfig, createdAt, updatedAt) VALUES (?, ?, ?, ?, ?, ?)",
).run(
"node_docker_config",
"docker-config-node",
"remote",
JSON.stringify({ image: "runfusion/fusion:latest", volumeMounts: [], environment: {}, configVersion: 1 }),
now,
now,
);
const insertedNode = db.prepare("SELECT dockerConfig FROM nodes WHERE id = ?").get("node_docker_config") as {
dockerConfig: string | null;
} | undefined;
expect(insertedNode?.dockerConfig).toBeTruthy();
});
});

View File

@@ -0,0 +1,179 @@
import { describe, expect, it, beforeEach, afterEach, vi } from "vitest";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
sanitizeDockerNodeConfigForResponse,
validateDockerNodeConfig,
type DockerNodeConfig,
} from "../types.js";
import { CentralCore } from "../central-core.js";
function createValidConfig(): DockerNodeConfig {
return {
image: "runfusion/fusion:latest",
containerName: "fusion-test",
volumeMounts: [{ hostPath: "fusion-data", containerPath: "/data", mode: "rw", type: "volume" }],
environment: { PLAIN: "value", API_KEY: "secret" },
resources: { memoryBytes: 2147483648, cpuCount: 2, pidsLimit: 256 },
host: {
contextName: "default",
dockerHost: "tcp://127.0.0.1:2376",
tlsCaCert: "/certs/ca.pem",
tlsCert: "/certs/cert.pem",
tlsKey: "/certs/key.pem",
tlsVerify: true,
},
extraClis: ["claude-cli"],
persistence: { volumeName: "fusion-data", retainOnDelete: true },
configVersion: 1,
lastUpdated: "2026-05-01T00:00:00.000Z",
};
}
describe("docker node config validation", () => {
it("passes validation for valid config", () => {
expect(validateDockerNodeConfig(createValidConfig()).valid).toBe(true);
});
it("returns errors for missing required fields", () => {
const result = validateDockerNodeConfig({});
expect(result.valid).toBe(false);
expect(result.errors).toEqual(
expect.arrayContaining([
"image must be a non-empty string",
"volumeMounts must be an array",
"environment must be an object",
"configVersion must be a number >= 1",
]),
);
});
it("returns errors for invalid field types", () => {
const result = validateDockerNodeConfig({
image: "ok",
volumeMounts: [{ hostPath: 123, containerPath: false }],
environment: { OK: "1", BAD: 2 },
configVersion: 1,
resources: { memoryBytes: "bad" },
host: { tlsVerify: "nope" },
persistence: { retainOnDelete: "nope" },
extraClis: ["ok", 1],
});
expect(result.valid).toBe(false);
expect(result.errors).toEqual(
expect.arrayContaining([
"volumeMounts[0].hostPath must be a string",
"volumeMounts[0].containerPath must be a string",
"environment.BAD must be a string value",
"resources.memoryBytes must be a number",
]),
);
});
it("requires configVersion >= 1", () => {
const base = createValidConfig();
expect(validateDockerNodeConfig({ ...base, configVersion: 0 }).valid).toBe(false);
expect(validateDockerNodeConfig({ ...base, configVersion: -1 }).valid).toBe(false);
});
});
describe("docker node config sanitization", () => {
it("masks sensitive env vars and tls key path without mutating input", () => {
const config = createValidConfig();
const sanitized = sanitizeDockerNodeConfigForResponse(config);
expect(sanitized.environment.API_KEY).toBe("***");
expect(sanitized.host?.tlsKey).toBe("***");
expect(config.environment.API_KEY).toBe("secret");
});
it("masks sensitive env vars case-insensitively and preserves non-sensitive values", () => {
const config = createValidConfig();
config.environment = { plain: "value", service_token: "token", DB_PASSWORD: "password" };
const sanitized = sanitizeDockerNodeConfigForResponse(config);
expect(sanitized.environment.plain).toBe("value");
expect(sanitized.environment.service_token).toBe("***");
expect(sanitized.environment.DB_PASSWORD).toBe("***");
});
});
describe("docker node config persistence", () => {
let tempDir: string;
let central: CentralCore;
beforeEach(async () => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2026-05-01T10:00:00.000Z"));
tempDir = mkdtempSync(join(tmpdir(), "kb-docker-node-config-test-"));
central = new CentralCore(tempDir);
await central.init();
});
afterEach(async () => {
await central.close();
vi.useRealTimers();
rmSync(tempDir, { recursive: true, force: true });
});
const baseConfig = (): DockerNodeConfig => ({
image: "runfusion/fusion:latest",
volumeMounts: [{ hostPath: "fusion-data", containerPath: "/data" }],
environment: { MODE: "docker" },
configVersion: 0,
});
it("register/get/update/list roundtrip with versioning semantics", async () => {
const created = await central.registerNode({
name: "docker-node",
type: "remote",
url: "http://127.0.0.1:4041",
apiKey: "key",
dockerConfig: baseConfig(),
});
expect(created.dockerConfig?.configVersion).toBe(1);
const sameOnPartial = await central.updateNode(created.id, { name: "docker-node-2" });
expect(sameOnPartial.dockerConfig?.configVersion).toBe(1);
vi.setSystemTime(new Date("2026-05-01T11:00:00.000Z"));
const updated = await central.updateNode(created.id, {
dockerConfig: { ...baseConfig(), image: "runfusion/fusion:v2", configVersion: 99 },
});
expect(updated.dockerConfig?.configVersion).toBe(2);
expect(updated.dockerConfig?.lastUpdated).toBe("2026-05-01T11:00:00.000Z");
const cleared = await central.updateNode(created.id, { dockerConfig: null });
expect(cleared.dockerConfig).toBeUndefined();
const reset = await central.updateNode(created.id, { dockerConfig: { ...baseConfig(), configVersion: 50 } });
expect(reset.dockerConfig?.configVersion).toBe(1);
const list = await central.listNodes();
expect(list.find((n) => n.id === created.id)?.dockerConfig?.image).toBe("runfusion/fusion:latest");
});
it("registering without docker config keeps field undefined", async () => {
const created = await central.registerNode({
name: "plain-node",
type: "remote",
url: "http://127.0.0.1:4042",
apiKey: "key",
});
expect((await central.getNode(created.id))?.dockerConfig).toBeUndefined();
});
it("throws on invalid docker config update", async () => {
const created = await central.registerNode({
name: "docker-invalid",
type: "remote",
url: "http://127.0.0.1:4043",
apiKey: "key",
dockerConfig: baseConfig(),
});
await expect(
central.updateNode(created.id, {
dockerConfig: { ...baseConfig(), image: "", configVersion: 1 },
}),
).rejects.toThrow("Invalid Docker config");
});
});