feat(FN-3769): align TaskCard controls with design tokens and add registrat

Adds comprehensive contract tests for registration, config flow, dashboard views, runtime availability, and workflow integration — including shared test fixtures for exec mocking and registry setup — along with a design-token alignment fix to TaskCard controls.

Fusion-Task-Id: FN-3769
This commit is contained in:
Fusion
2026-05-10 22:25:42 -07:00
committed by gsxdsm
parent 2b77b71674
commit 005b5d08ce
9 changed files with 459 additions and 8 deletions

View File

@@ -0,0 +1,80 @@
import { execSync } from "node:child_process";
import { expect, vi } from "vitest";
type ExecResult = {
stdout?: string;
stderr?: string;
code?: number;
timeoutAfterMs?: number;
};
type ExecCall = {
command: string;
options: Record<string, unknown>;
};
const calls: ExecCall[] = [];
const queue: ExecResult[] = [];
vi.mock("node:child_process", () => {
const execMock = vi.fn((command: string, options: Record<string, unknown>, callback: (err: Error | null, stdout: string, stderr: string) => void) => {
calls.push({ command, options });
const next = queue.shift() ?? { stdout: "", stderr: "", code: 0 };
if (next.timeoutAfterMs) {
const err = new Error(`Command timed out after ${next.timeoutAfterMs}ms`) as Error & { killed?: boolean; signal?: string };
err.killed = true;
err.signal = "SIGTERM";
callback(err, next.stdout ?? "", next.stderr ?? "");
return { kill: () => true };
}
if ((next.code ?? 0) !== 0) {
const err = new Error(next.stderr || `Command failed with code ${next.code}`) as Error & {
code?: number;
stdout?: string;
stderr?: string;
};
err.code = next.code;
err.stdout = next.stdout ?? "";
err.stderr = next.stderr ?? "";
callback(err, next.stdout ?? "", next.stderr ?? "");
return { kill: () => true };
}
callback(null, next.stdout ?? "", next.stderr ?? "");
return { kill: () => true };
});
execMock[Symbol.for("nodejs.util.promisify.custom")] = (command: string, options: Record<string, unknown>) =>
new Promise<{ stdout: string; stderr: string }>((resolve, reject) => {
execMock(command, options, (error: Error | null, stdout: string, stderr: string) => {
if (error) {
reject(error);
return;
}
resolve({ stdout, stderr });
});
});
return {
exec: execMock,
execSync: vi.fn(() => {
throw new Error("execSync should never be called");
}),
};
});
export function installExecMock() {
calls.length = 0;
queue.length = 0;
return {
setNextResult(result: ExecResult) {
queue.push(result);
},
getCalls() {
return [...calls];
},
assertExecSyncUnused() {
expect(execSync).not.toHaveBeenCalled();
},
};
}

View File

@@ -0,0 +1,40 @@
import { promisify } from "node:util";
import { describe, expect, it } from "vitest";
import { makeFakeRegistry } from "./registry.js";
import { installExecMock } from "./exec-mock.js";
describe("test fixtures", () => {
it("creates seeded fake registry", () => {
const registry = makeFakeRegistry();
try {
expect(registry.store.listServices().map((s) => s.slug).sort()).toEqual(["acme", "beta"]);
expect(registry.store.listArtifacts(registry.specs.acme.id)).toHaveLength(1);
expect(registry.store.listArtifacts(registry.specs.beta.id)).toHaveLength(0);
} finally {
registry.cleanup();
}
});
it("records mocked exec calls", async () => {
const execMock = installExecMock();
execMock.setNextResult({ stdout: "ok" });
const { exec } = await import("node:child_process");
const execAsync = promisify(exec);
const result = await execAsync("node --version", { cwd: "/tmp" });
expect(result.stdout).toBe("ok");
expect(execMock.getCalls()).toEqual([{ command: "node --version", options: { cwd: "/tmp" } }]);
execMock.assertExecSyncUnused();
});
it("simulates timeout and blocks execSync", async () => {
const execMock = installExecMock();
execMock.setNextResult({ timeoutAfterMs: 25, stderr: "timed out" });
const { exec, execSync } = await import("node:child_process");
const execAsync = promisify(exec);
await expect(execAsync("echo slow", { timeout: 25 })).rejects.toThrow(/timed out/i);
expect(() => execSync("echo no")).toThrow("execSync should never be called");
});
});

View File

@@ -0,0 +1,82 @@
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Database } from "@fusion/core";
import { createCliPressStore } from "../../store/cli-press-store.js";
import { encodeCredentialValue } from "../../store/credentials.js";
export function makeFakeRegistry() {
const rootDir = mkdtempSync(join(tmpdir(), "cli-printing-press-registry-"));
const db = new Database(join(rootDir, ".fusion"), { inMemory: true });
db.init();
const store = createCliPressStore(db);
const acme = store.createService({
slug: "acme",
displayName: "Acme Service",
description: "Acme CLI",
baseUrl: "https://acme.example.com",
sourceKind: "manual",
});
const acmeSpec = store.createSpec({
serviceId: acme.id,
name: "acme-cli",
version: "1.0.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify({ id: acme.id, slug: acme.slug }),
status: "generated",
generatedAt: new Date().toISOString(),
lastGenerationError: undefined,
});
const acmePath = `plugins/cli-printing-press/artifacts/${acme.id}/${acmeSpec.id}/acme`;
const acmeAbsPath = join(rootDir, ".fusion", acmePath);
mkdirSync(join(acmeAbsPath, ".."), { recursive: true });
writeFileSync(acmeAbsPath, "#!/bin/sh\necho acme\n");
store.createArtifact({ cliSpecId: acmeSpec.id, kind: "script", path: acmePath, executable: true });
store.createCredential({
serviceId: acme.id,
name: "token",
kind: "env_var",
placement: { kind: "env_var", envVar: "ACME_TOKEN" },
value: encodeCredentialValue("acme-secret"),
});
const beta = store.createService({
slug: "beta",
displayName: "Beta Service",
description: "Beta CLI",
baseUrl: "https://beta.example.com",
sourceKind: "manual",
});
const betaSpec = store.createSpec({
serviceId: beta.id,
name: "beta-cli",
version: "1.0.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify({ id: beta.id, slug: beta.slug, unbuilt: true }),
status: "draft",
generatedAt: undefined,
lastGenerationError: undefined,
});
store.createCredential({
serviceId: beta.id,
name: "header-token",
kind: "header",
placement: { kind: "header", header: "X-Beta-Token" },
value: encodeCredentialValue("beta-secret"),
});
const cleanup = () => {
db.close();
rmSync(rootDir, { recursive: true, force: true });
};
return {
rootDir,
db,
store,
services: { acme, beta },
specs: { acme: acmeSpec, beta: betaSpec },
cleanup,
};
}