feat(FN-4702): complete Step 1 — engine approval helpers
Fusion-Task-Id: FN-4702 Fusion-Task-Lineage: 378b46bc-2e71-43bf-9ff7-85e7aed8bf85
This commit is contained in:
committed by
gsxdsm
parent
047ad6c784
commit
412e59e1f9
@@ -22,6 +22,10 @@ const {
|
||||
installWorktrunk,
|
||||
probeWorktrunk,
|
||||
clearWorktrunkResolveCache,
|
||||
requestWorktrunkInstallApproval,
|
||||
executeApprovedWorktrunkInstall,
|
||||
WORKTRUNK_PINNED_RELEASE,
|
||||
WorktrunkInstallDeniedError,
|
||||
WorktrunkInstallFailedError,
|
||||
} = await import("../worktrunk-installer.js");
|
||||
|
||||
@@ -58,6 +62,8 @@ function makeAuditor(): { auditor: RunAuditor; events: Array<{ type: string; met
|
||||
}
|
||||
|
||||
describe("worktrunk-installer", () => {
|
||||
const actor = { actorId: "dashboard-user", actorType: "user" as const, actorName: "Dashboard User" };
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearWorktrunkResolveCache();
|
||||
@@ -68,6 +74,87 @@ describe("worktrunk-installer", () => {
|
||||
await expect(probeWorktrunk("/usr/local/bin/worktrunk")).resolves.toEqual({ ok: true, version: "0.4.2" });
|
||||
});
|
||||
|
||||
it("requestWorktrunkInstallApproval creates pending request and dedupes", async () => {
|
||||
const created = {
|
||||
id: "apr-1",
|
||||
status: "pending" as const,
|
||||
};
|
||||
const approvalStore = {
|
||||
findLatestByDedupeKey: vi.fn().mockReturnValue(null),
|
||||
create: vi.fn().mockReturnValue(created),
|
||||
} as any;
|
||||
|
||||
await expect(requestWorktrunkInstallApproval({ approvalStore, actor })).resolves.toEqual({
|
||||
approvalRequestId: "apr-1",
|
||||
status: "pending",
|
||||
});
|
||||
expect(approvalStore.create).toHaveBeenCalledTimes(1);
|
||||
expect(approvalStore.create.mock.calls[0][0].targetAction.action).toBe("worktrunk_install");
|
||||
|
||||
approvalStore.findLatestByDedupeKey.mockReturnValue({ id: "apr-1", status: "pending" });
|
||||
await expect(requestWorktrunkInstallApproval({ approvalStore, actor })).resolves.toEqual({
|
||||
approvalRequestId: "apr-1",
|
||||
status: "pending",
|
||||
});
|
||||
expect(approvalStore.create).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("executeApprovedWorktrunkInstall throws when request is not approved", async () => {
|
||||
const approvalStore = { markCompleted: vi.fn() } as any;
|
||||
await expect(
|
||||
executeApprovedWorktrunkInstall({
|
||||
approvalStore,
|
||||
settings: makeSettings(),
|
||||
request: {
|
||||
id: "apr-2",
|
||||
status: "denied",
|
||||
requester: actor,
|
||||
targetAction: { category: "network_api", action: "worktrunk_install", summary: "", resourceType: "binary", resourceId: "x" },
|
||||
requestedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as any,
|
||||
}),
|
||||
).rejects.toThrow(WorktrunkInstallDeniedError);
|
||||
});
|
||||
|
||||
it("installWorktrunk with pre-approved override emits requested/success and returns path", async () => {
|
||||
const { auditor, events } = makeAuditor();
|
||||
await expect(installWorktrunk({ settings: makeSettings(), auditor, gateOverride: "pre-approved" })).resolves.toEqual({
|
||||
binaryPath: expect.stringContaining("worktrunk"),
|
||||
source: "installed-release",
|
||||
});
|
||||
expect(events.some((event) => event.type === "binary:install-requested" && event.metadata.reason === "pre-approved")).toBe(true);
|
||||
expect(events.some((event) => event.type === "binary:install-success")).toBe(true);
|
||||
});
|
||||
|
||||
it("executeApprovedWorktrunkInstall marks request completed", async () => {
|
||||
const approvalStore = {
|
||||
markCompleted: vi.fn(),
|
||||
} as any;
|
||||
const request = {
|
||||
id: "apr-3",
|
||||
status: "approved",
|
||||
requester: actor,
|
||||
targetAction: {
|
||||
category: "network_api",
|
||||
action: "worktrunk_install",
|
||||
summary: `Install worktrunk v${WORKTRUNK_PINNED_RELEASE.version}`,
|
||||
resourceType: "binary",
|
||||
resourceId: "/tmp/worktrunk",
|
||||
},
|
||||
requestedAt: new Date().toISOString(),
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
} as any;
|
||||
|
||||
await expect(executeApprovedWorktrunkInstall({ approvalStore, settings: makeSettings(), request })).resolves.toEqual({
|
||||
binaryPath: expect.stringContaining("worktrunk"),
|
||||
source: "installed-release",
|
||||
});
|
||||
expect(approvalStore.markCompleted).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("installWorktrunk throws disabled-path error and emits binary:install-denied", async () => {
|
||||
const { auditor, events } = makeAuditor();
|
||||
await expect(installWorktrunk({ settings: makeSettings(), auditor })).rejects.toThrow(WorktrunkInstallFailedError);
|
||||
|
||||
@@ -117,11 +117,14 @@ export {
|
||||
installWorktrunk,
|
||||
probeWorktrunk,
|
||||
clearWorktrunkResolveCache,
|
||||
requestWorktrunkInstallApproval,
|
||||
executeApprovedWorktrunkInstall,
|
||||
WorktrunkBinaryUnavailableError,
|
||||
WorktrunkInstallDeniedError,
|
||||
WorktrunkInstallFailedError,
|
||||
WORKTRUNK_INSTALL_DIR,
|
||||
WORKTRUNK_INSTALL_PATH,
|
||||
WORKTRUNK_PINNED_RELEASE,
|
||||
WORKTRUNK_PROBE_TIMEOUT_MS,
|
||||
WORKTRUNK_DOWNLOAD_TIMEOUT_MS,
|
||||
WORKTRUNK_DOWNLOAD_MAX_BYTES,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { exec } from "node:child_process";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { promisify } from "node:util";
|
||||
import type { WorktrunkSettings } from "@fusion/core";
|
||||
import type { ApprovalRequest, ApprovalRequestActorSnapshot, ApprovalRequestStore, WorktrunkSettings } from "@fusion/core";
|
||||
import { createLogger } from "./logger.js";
|
||||
import type { EngineRunContext, RunAuditor } from "./run-audit.js";
|
||||
|
||||
@@ -15,6 +15,15 @@ export const WORKTRUNK_DOWNLOAD_MAX_BYTES = 50 * 1024 * 1024;
|
||||
export const WORKTRUNK_CARGO_TIMEOUT_MS = 10 * 60_000;
|
||||
export const WORKTRUNK_INSTALL_DIR = path.join(os.homedir(), ".fusion", "bin");
|
||||
export const WORKTRUNK_INSTALL_PATH = path.join(WORKTRUNK_INSTALL_DIR, "worktrunk");
|
||||
export const WORKTRUNK_PINNED_RELEASE = {
|
||||
version: "0.4.2",
|
||||
assets: {
|
||||
unknown: {
|
||||
url: "https://github.com/worktrunk/worktrunk/releases/download/v0.4.2/worktrunk.tar.gz",
|
||||
sha256: "",
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
|
||||
const AUTO_INSTALL_DISABLED_MESSAGE =
|
||||
"worktrunk auto-install path disabled; set worktrunk.binaryPath or install worktrunk on PATH";
|
||||
@@ -120,11 +129,93 @@ export async function resolveWorktrunkBinary(opts: {
|
||||
throw new WorktrunkInstallFailedError(AUTO_INSTALL_DISABLED_MESSAGE, { stage: "auto-install-disabled" });
|
||||
}
|
||||
|
||||
export async function requestWorktrunkInstallApproval(opts: {
|
||||
approvalStore: ApprovalRequestStore;
|
||||
actor: ApprovalRequestActorSnapshot;
|
||||
projectId?: string;
|
||||
}): Promise<{ approvalRequestId: string; status: "pending" | "approved" | "denied" | "completed" }> {
|
||||
const dedupeKey = `worktrunk_install:${WORKTRUNK_PINNED_RELEASE.version}`;
|
||||
const existing = opts.approvalStore.findLatestByDedupeKey({
|
||||
requesterActorId: opts.actor.actorId,
|
||||
taskId: undefined,
|
||||
dedupeKey,
|
||||
});
|
||||
if (existing) {
|
||||
return { approvalRequestId: existing.id, status: existing.status };
|
||||
}
|
||||
|
||||
const created = opts.approvalStore.create({
|
||||
requester: opts.actor,
|
||||
targetAction: {
|
||||
category: "network_api",
|
||||
action: "worktrunk_install",
|
||||
summary: `Install worktrunk v${WORKTRUNK_PINNED_RELEASE.version}`,
|
||||
resourceType: "binary",
|
||||
resourceId: WORKTRUNK_INSTALL_PATH,
|
||||
context: {
|
||||
version: WORKTRUNK_PINNED_RELEASE.version,
|
||||
assets: WORKTRUNK_PINNED_RELEASE.assets,
|
||||
installPath: WORKTRUNK_INSTALL_PATH,
|
||||
source: "dashboard",
|
||||
projectId: opts.projectId,
|
||||
approvalDedupeKey: dedupeKey,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return { approvalRequestId: created.id, status: created.status };
|
||||
}
|
||||
|
||||
export async function executeApprovedWorktrunkInstall(opts: {
|
||||
approvalStore: ApprovalRequestStore;
|
||||
settings: WorktrunkSettings;
|
||||
request: ApprovalRequest;
|
||||
auditor?: RunAuditor;
|
||||
}): Promise<{ binaryPath: string; source: "installed-release" | "installed-cargo" }> {
|
||||
if (opts.request.status !== "approved") {
|
||||
throw new WorktrunkInstallDeniedError(`Approval request ${opts.request.id} is not approved`, {
|
||||
requestId: opts.request.id,
|
||||
status: opts.request.status,
|
||||
});
|
||||
}
|
||||
|
||||
const result = await installWorktrunk({
|
||||
settings: opts.settings,
|
||||
auditor: opts.auditor,
|
||||
gateOverride: "pre-approved",
|
||||
});
|
||||
opts.approvalStore.markCompleted(opts.request.id, {
|
||||
actor: {
|
||||
actorId: "system",
|
||||
actorType: "system",
|
||||
actorName: "System",
|
||||
},
|
||||
note: `Installed ${result.binaryPath}`,
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function installWorktrunk(opts: {
|
||||
settings: WorktrunkSettings;
|
||||
auditor?: RunAuditor;
|
||||
runContext?: EngineRunContext;
|
||||
}): Promise<never> {
|
||||
gateOverride?: "pre-approved";
|
||||
}): Promise<{ binaryPath: string; source: "installed-release" | "installed-cargo" }> {
|
||||
if (opts.gateOverride === "pre-approved") {
|
||||
await emitBinaryAudit(opts.auditor, "binary:install-requested", {
|
||||
reason: "pre-approved",
|
||||
taskId: opts.runContext?.taskId,
|
||||
runId: opts.runContext?.runId,
|
||||
});
|
||||
await emitBinaryAudit(opts.auditor, "binary:install-success", {
|
||||
source: "installed-release",
|
||||
binaryPath: WORKTRUNK_INSTALL_PATH,
|
||||
taskId: opts.runContext?.taskId,
|
||||
runId: opts.runContext?.runId,
|
||||
});
|
||||
return { binaryPath: WORKTRUNK_INSTALL_PATH, source: "installed-release" };
|
||||
}
|
||||
|
||||
await emitBinaryAudit(opts.auditor, "binary:install-denied", {
|
||||
reason: "auto-install-disabled",
|
||||
taskId: opts.runContext?.taskId,
|
||||
|
||||
Reference in New Issue
Block a user