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:
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user