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 0488e33b22
commit e81a204a46
9 changed files with 459 additions and 8 deletions

View File

@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";
import { makeFakeRegistry } from "./fixtures/registry.js";
import { InvalidCredentialPlacementError, OAuthNotSupportedError } from "../store/cli-press-types.js";
describe("cli press store config flow", () => {
it("round-trips service/spec/settings CRUD", () => {
const h = makeFakeRegistry();
try {
const created = h.store.createService({
slug: "gamma",
displayName: "Gamma Service",
description: "gamma",
baseUrl: "https://gamma.example.com",
sourceKind: "manual",
});
expect(h.store.getService(created.id)?.slug).toBe("gamma");
const spec = h.store.createSpec({
serviceId: created.id,
name: "gamma-cli",
version: "1.0.0",
generatorVersion: "cli-printing-press",
specJson: JSON.stringify({ id: created.id, regeneratedAt: "before" }),
status: "draft",
});
const updated = h.store.updateSpec(spec.id, {
status: "generated",
generatedAt: new Date().toISOString(),
specJson: JSON.stringify({ id: created.id, regeneratedAt: "after", artifactPath: "/tmp/gamma" }),
});
expect(updated.status).toBe("generated");
expect(JSON.parse(updated.specJson)).toMatchObject({ regeneratedAt: "after" });
const setting = h.store.setSetting({
serviceId: created.id,
key: "runner.timeoutMs",
value: "120000",
scope: "wizard",
});
expect(h.store.listSettings(created.id).find((entry) => entry.id === setting.id)?.value).toBe("120000");
h.store.deleteSpec(spec.id);
expect(h.store.getSpec(spec.id)).toBeUndefined();
h.store.deleteService(created.id);
expect(h.store.getService(created.id)).toBeUndefined();
} finally {
h.cleanup();
}
});
it("stores credential values encoded and does not expose raw secrets", () => {
const h = makeFakeRegistry();
try {
const acme = h.services.acme;
const credential = h.store.listCredentials(acme.id)[0];
expect(credential.value).toMatchObject({ encoding: "base64" });
expect(JSON.stringify(credential.value)).not.toContain("acme-secret");
} finally {
h.cleanup();
}
});
it("rejects oauth credentials and mismatched placement", () => {
const h = makeFakeRegistry();
try {
const serviceId = h.services.acme.id;
expect(() =>
h.store.createCredential({
serviceId,
name: "oauth",
kind: "oauth",
placement: { kind: "oauth", provider: "acme" } as never,
value: { encoding: "base64", value: "abc" },
} as never),
).toThrow(OAuthNotSupportedError);
expect(() =>
h.store.createCredential({
serviceId,
name: "bad-placement",
kind: "header",
placement: { kind: "query_param", queryParam: "token" } as never,
value: { encoding: "base64", value: "abc" },
} as never),
).toThrow(InvalidCredentialPlacementError);
} finally {
h.cleanup();
}
});
});

View File

@@ -0,0 +1,46 @@
import { describe, expect, it } from "vitest";
import { createCliPrintingPressRoutes } from "../routes/wizard-routes.js";
import { makeFakeRegistry } from "./fixtures/registry.js";
function route(method: string, path: string) {
const found = createCliPrintingPressRoutes().find((entry) => entry.method === method && entry.path === path);
if (!found) throw new Error(`missing route ${method} ${path}`);
return found;
}
describe("dashboard API route contracts", () => {
it("defines expected wizard/list/detail/run endpoints", () => {
const routes = createCliPrintingPressRoutes();
expect(routes.map((entry) => `${entry.method} ${entry.path}`)).toEqual(
expect.arrayContaining([
"POST /drafts",
"GET /drafts",
"GET /drafts/:id",
"PUT /drafts/:id",
"POST /drafts/:id/regenerate",
"POST /drafts/:id/run",
]),
);
});
it("supports happy and error API paths", async () => {
const h = makeFakeRegistry();
try {
const ctx = { taskStore: { getRootDir: () => h.rootDir, getDatabase: () => h.db } } as any;
const listRes = await route("GET", "/drafts").handler({ params: {} }, ctx);
expect(listRes.status).toBe(200);
const missRes = await route("GET", "/drafts/:id").handler({ params: { id: "missing" } }, ctx);
expect(missRes.status).toBe(404);
const badRun = await route("POST", "/drafts/:id/run").handler({ params: { id: h.services.acme.id }, body: { endpointId: "", params: {} } }, ctx);
expect(badRun.status).toBe(400);
} finally {
h.cleanup();
}
});
it("documents plugin-prefixed mount contract", () => {
expect("/api/plugins/cli-printing-press/drafts").toContain("/api/plugins/cli-printing-press/");
});
});

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,
};
}

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { validatePluginManifest } from "@fusion/core";
import plugin from "../index.js";
import { ensureCliPressSchema } from "../store/cli-press-store.js";
import { makeFakeRegistry } from "./fixtures/registry.js";
describe("plugin registration contracts", () => {
it("declares expected manifest and semver version", () => {
expect(plugin.manifest.id).toBe("fusion-plugin-cli-printing-press");
expect(plugin.manifest.version).toMatch(/^\d+\.\d+\.\d+$/);
expect(validatePluginManifest(plugin.manifest).valid).toBe(true);
});
it("registers schema, routes, dashboard views and executor runtime hook", () => {
const h = makeFakeRegistry();
try {
expect(() => ensureCliPressSchema(h.db)).not.toThrow();
expect(plugin.routes?.some((route) => route.path === "/drafts")).toBe(true);
expect(plugin.dashboardViews?.map((view) => view.viewId)).toEqual(["wizard", "manage"]);
expect(typeof plugin.executorRuntimeEnv).toBe("function");
} finally {
h.cleanup();
}
});
it.skip("TODO(FN-3768): assert plugin workflow-step template contributions once shipped", () => {
// This branch does not yet export workflow step templates/contributions for cli-printing-press.
});
});

View File

@@ -0,0 +1,61 @@
import { describe, expect, it } from "vitest";
import plugin from "../index.js";
import { buildExecutorRuntimeEnv } from "../runtime/executor-runtime-env.js";
import { makeFakeRegistry } from "./fixtures/registry.js";
describe("runtime availability", () => {
it("exposes executor runtime env hook through plugin entry", () => {
expect(typeof plugin.executorRuntimeEnv).toBe("function");
});
it("returns PATH/env entries for generated CLIs only", () => {
const h = makeFakeRegistry();
try {
const result = buildExecutorRuntimeEnv(
h.store,
{ taskId: "FN-3769", worktreePath: h.rootDir, rootDir: h.rootDir },
{
pluginId: "fusion-plugin-cli-printing-press",
taskStore: {} as never,
settings: {},
logger: { info() {}, warn() {}, error() {}, debug() {} },
emitEvent() {},
},
);
expect(result.pathPrepend).toHaveLength(1);
expect(result.pathPrepend[0]).toContain(`/artifacts/${h.services.acme.id}/${h.specs.acme.id}`);
expect(result.env).toEqual({ ACME_TOKEN: "acme-secret" });
} finally {
h.cleanup();
}
});
it("skips draft specs from PATH contributions", () => {
const h = makeFakeRegistry();
try {
const betaArtifacts = h.store.listArtifacts(h.specs.beta.id);
expect(betaArtifacts).toHaveLength(0);
const result = buildExecutorRuntimeEnv(
h.store,
{ taskId: "FN-3769", worktreePath: h.rootDir, rootDir: h.rootDir },
{
pluginId: "fusion-plugin-cli-printing-press",
taskStore: {} as never,
settings: {},
logger: { info() {}, warn() {}, error() {}, debug() {} },
emitEvent() {},
},
);
expect(result.pathPrepend.some((entry) => entry.includes(`/artifacts/${h.services.beta.id}/`))).toBe(false);
} finally {
h.cleanup();
}
});
it.skip("TODO(FN-3767): cover resolveGeneratedCliInvocation contract once exported", () => {
// FN-3767 documented resolveGeneratedCliInvocation, but this branch has only buildExecutorRuntimeEnv.
});
});

View File

@@ -0,0 +1,19 @@
import { describe, expect, it } from "vitest";
import plugin from "../index.js";
import { installExecMock } from "./fixtures/exec-mock.js";
describe("workflow integration contracts", () => {
it("guards against execSync usage in workflow-oriented execution fixtures", () => {
const execMock = installExecMock();
execMock.assertExecSyncUnused();
expect(typeof plugin.manifest.id).toBe("string");
});
it.skip("TODO(FN-3768): execute script-mode workflow handler once plugin workflow step contributions are available", () => {
// Missing in this branch: cli-printing-press workflow step contribution + script handler wiring.
});
it.skip("TODO(FN-3768): run through runWorkflowSteps with plugin:cli-printing-press:<id>", () => {
// packages/core resolvePluginWorkflowStep currently hard-codes mode="prompt".
});
});