feat(FN-3448): normalize AgentsView button utility classes
The merge normalizes button utility classes in AgentsView, swapping 12 lines of CSS class references for their standardized counterparts for consistency and maintainability. Fusion-Task-Id: FN-3448
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
SHARED_MESH_PROTOCOL_ID,
|
||||
SHARED_MESH_PROTOCOL_VERSION,
|
||||
classifyReadStaleness,
|
||||
createFenceToken,
|
||||
getCoordinationModeForEntity,
|
||||
getDefaultWriteClassForEntity,
|
||||
getQuorumRequirement,
|
||||
isMeshIntentState,
|
||||
isMeshWriteClass,
|
||||
isProtocolRef,
|
||||
isQuorumSatisfied,
|
||||
} from "../mesh-replication-protocol.js";
|
||||
|
||||
describe("mesh-replication-protocol", () => {
|
||||
it("exposes protocol identity", () => {
|
||||
expect(SHARED_MESH_PROTOCOL_ID).toBe("fusion.shared-mesh");
|
||||
expect(SHARED_MESH_PROTOCOL_VERSION).toBe("1.0");
|
||||
});
|
||||
|
||||
it("classifies entity coordination modes", () => {
|
||||
expect(getCoordinationModeForEntity("task")).toBe("strongly-coordinated");
|
||||
expect(getCoordinationModeForEntity("audit-event")).toBe("append-only-replicated");
|
||||
expect(getCoordinationModeForEntity("filesystem-blob")).toBe("queued-for-later");
|
||||
expect(getCoordinationModeForEntity("agent-runtime")).toBe("node-local-only");
|
||||
});
|
||||
|
||||
it("maps entity coordination mode to default write class", () => {
|
||||
expect(getDefaultWriteClassForEntity("task")).toBe("strong");
|
||||
expect(getDefaultWriteClassForEntity("audit-event")).toBe("append-only");
|
||||
expect(getDefaultWriteClassForEntity("filesystem-blob")).toBe("queued");
|
||||
expect(getDefaultWriteClassForEntity("agent-runtime")).toBe("local");
|
||||
});
|
||||
|
||||
it("computes quorum requirements and satisfies majority rule", () => {
|
||||
expect(getQuorumRequirement(1)).toEqual({ eligibleVoters: 1, requiredAcks: 1 });
|
||||
expect(getQuorumRequirement(2)).toEqual({ eligibleVoters: 2, requiredAcks: 2 });
|
||||
expect(getQuorumRequirement(3)).toEqual({ eligibleVoters: 3, requiredAcks: 2 });
|
||||
expect(isQuorumSatisfied(5, 2)).toBe(false);
|
||||
expect(isQuorumSatisfied(5, 3)).toBe(true);
|
||||
});
|
||||
|
||||
it("validates write class and intent state discriminators", () => {
|
||||
expect(isMeshWriteClass("strong")).toBe(true);
|
||||
expect(isMeshWriteClass("invalid")).toBe(false);
|
||||
expect(isMeshIntentState("committed")).toBe(true);
|
||||
expect(isMeshIntentState("waiting")).toBe(false);
|
||||
});
|
||||
|
||||
it("creates deterministic fence tokens", () => {
|
||||
expect(createFenceToken(7, "node_a", 99)).toBe("7:node_a:99");
|
||||
});
|
||||
|
||||
it("validates protocol refs", () => {
|
||||
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "1.0" })).toBe(true);
|
||||
expect(isProtocolRef({ protocol: "fusion.shared-mesh", version: "2.0" })).toBe(false);
|
||||
});
|
||||
|
||||
it("classifies read staleness from queue depth and lag", () => {
|
||||
const fresh = classifyReadStaleness({ queueDepth: 0, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:10.000Z" });
|
||||
expect(fresh.isStale).toBe(false);
|
||||
expect(fresh.source).toBe("local-committed");
|
||||
|
||||
const queued = classifyReadStaleness({ queueDepth: 2, observedAt: "2026-05-05T00:00:10.000Z", lastGlobalCommitAt: "2026-05-05T00:00:09.000Z" });
|
||||
expect(queued.isStale).toBe(true);
|
||||
expect(queued.source).toBe("local-queued");
|
||||
expect(queued.replicationLagMs).toBe(1000);
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, isEphemeralAgent, hasAgentIdentity, CheckoutConflictError, DEFAULT_HEARTBEAT_PROCEDURE_PATH, getDefaultHeartbeatProcedurePath, EXECUTION_MODES, DEFAULT_EXECUTION_MODE, TASK_PRIORITIES, DEFAULT_TASK_PRIORITY, validateMessageMetadata, validateDockerNodeConfig, sanitizeDockerNodeConfigForResponse, normalizeMergeConflictStrategy, buildResearchDocumentKey } from "./types.js";
|
||||
export type { Column, IssueInfo, IssueState, TaskSourceIssue, PrInfo, PrStatus, Task, TaskTokenUsage, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskDocumentWithTask, TaskCreateInput, TaskSource, SourceType, TaskDetail, InboxTask, TodoList, TodoItem, TodoListCreateInput, TodoListUpdateInput, TodoItemCreateInput, TodoItemUpdateInput, TodoListWithItems, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, MergeConflictStrategy, CanonicalMergeConflictStrategy, Settings, GlobalSettings, ProjectSettings, WebSearchBackend, ResearchEnabledSources, ResearchGlobalDefaults, ResearchProjectLimits, ResearchProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, ExecutionMode, TaskPriority, UnavailableNodePolicy, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, NotificationEvent, NotificationPayload, NotificationProviderConfig, CustomProvider, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, MessageMetadata, MessageReplyReference, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||
export * from "./mesh-replication-protocol.js";
|
||||
export {
|
||||
BUILTIN_AGENT_PROMPTS,
|
||||
resolveAgentPrompt,
|
||||
|
||||
179
packages/core/src/mesh-replication-protocol.ts
Normal file
179
packages/core/src/mesh-replication-protocol.ts
Normal file
@@ -0,0 +1,179 @@
|
||||
import type { NodeMeshState, PeerSyncRequest, PeerSyncResponse, SettingsSyncPayload } from "./types.js";
|
||||
|
||||
export const SHARED_MESH_PROTOCOL_ID = "fusion.shared-mesh" as const;
|
||||
export const SHARED_MESH_PROTOCOL_VERSION = "1.0" as const;
|
||||
|
||||
export const MESH_WRITE_CLASSES = ["strong", "append-only", "queued", "local"] as const;
|
||||
export type MeshWriteClass = (typeof MESH_WRITE_CLASSES)[number];
|
||||
|
||||
export const MESH_INTENT_STATES = ["intent", "committed", "rejected", "queued", "reconciled"] as const;
|
||||
export type MeshIntentState = (typeof MESH_INTENT_STATES)[number];
|
||||
|
||||
export const MESH_RECONCILIATION_OUTCOMES = [
|
||||
"applied",
|
||||
"noop_already_applied",
|
||||
"superseded",
|
||||
"conflict_requires_merge",
|
||||
"rejected_fenced",
|
||||
] as const;
|
||||
export type MeshReconciliationOutcome = (typeof MESH_RECONCILIATION_OUTCOMES)[number];
|
||||
|
||||
export type SharedMeshEntityType =
|
||||
| "task"
|
||||
| "task-metadata"
|
||||
| "mission"
|
||||
| "agent-config"
|
||||
| "agent-runtime"
|
||||
| "project-settings"
|
||||
| "auth-material"
|
||||
| "execution-run"
|
||||
| "audit-event"
|
||||
| "filesystem-blob";
|
||||
|
||||
export type SharedMeshCoordinationMode = "strongly-coordinated" | "append-only-replicated" | "queued-for-later" | "node-local-only";
|
||||
|
||||
export interface SharedMeshProtocolRef {
|
||||
protocol: typeof SHARED_MESH_PROTOCOL_ID;
|
||||
version: typeof SHARED_MESH_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
export interface SharedMeshLeaseRef {
|
||||
leaseEpoch: number;
|
||||
fenceToken: string;
|
||||
coordinatorNodeId: string;
|
||||
}
|
||||
|
||||
export interface SharedMeshWritePrecondition {
|
||||
expectedBaseRevision?: string;
|
||||
expectedLeaseEpoch?: number;
|
||||
}
|
||||
|
||||
export interface SharedMeshReplicationEnvelope<TPayload = unknown> extends SharedMeshProtocolRef, SharedMeshLeaseRef {
|
||||
recordId: string;
|
||||
intentId: string;
|
||||
entityType: SharedMeshEntityType;
|
||||
entityId: string;
|
||||
originNodeId: string;
|
||||
originSeq: number;
|
||||
writeClass: MeshWriteClass;
|
||||
state: MeshIntentState;
|
||||
createdAt: string;
|
||||
committedAt?: string;
|
||||
precondition?: SharedMeshWritePrecondition;
|
||||
payload: TPayload;
|
||||
}
|
||||
|
||||
export interface SharedMeshQueueEntryMeta {
|
||||
firstAttemptAt: string;
|
||||
lastAttemptAt?: string;
|
||||
nextAttemptAt?: string;
|
||||
retryCount: number;
|
||||
}
|
||||
|
||||
export interface SharedMeshReconciliationResult {
|
||||
intentId: string;
|
||||
outcome: MeshReconciliationOutcome;
|
||||
detail?: string;
|
||||
}
|
||||
|
||||
export interface SharedMeshReadStaleness {
|
||||
source: "local-committed" | "local-queued" | "replica";
|
||||
lastGlobalCommitAt?: string;
|
||||
replicationLagMs?: number;
|
||||
queueDepth: number;
|
||||
isStale: boolean;
|
||||
}
|
||||
|
||||
export interface SharedMeshQuorumRequirement {
|
||||
eligibleVoters: number;
|
||||
requiredAcks: number;
|
||||
}
|
||||
|
||||
export type SharedMeshSyncRequestEnvelope<TPayload = unknown> = PeerSyncRequest & {
|
||||
replication?: SharedMeshReplicationEnvelope<TPayload>[];
|
||||
};
|
||||
|
||||
export type SharedMeshSyncResponseEnvelope<TPayload = unknown> = PeerSyncResponse & {
|
||||
replication?: SharedMeshReplicationEnvelope<TPayload>[];
|
||||
};
|
||||
|
||||
export interface SharedMeshSettingsRecord {
|
||||
settings: SettingsSyncPayload;
|
||||
}
|
||||
|
||||
export interface SharedMeshSnapshot {
|
||||
mesh: NodeMeshState;
|
||||
staleness: SharedMeshReadStaleness;
|
||||
}
|
||||
|
||||
const COORDINATION_BY_ENTITY: Record<SharedMeshEntityType, SharedMeshCoordinationMode> = {
|
||||
task: "strongly-coordinated",
|
||||
"task-metadata": "strongly-coordinated",
|
||||
mission: "strongly-coordinated",
|
||||
"agent-config": "strongly-coordinated",
|
||||
"agent-runtime": "node-local-only",
|
||||
"project-settings": "strongly-coordinated",
|
||||
"auth-material": "queued-for-later",
|
||||
"execution-run": "queued-for-later",
|
||||
"audit-event": "append-only-replicated",
|
||||
"filesystem-blob": "queued-for-later",
|
||||
};
|
||||
|
||||
export function isMeshWriteClass(value: string): value is MeshWriteClass {
|
||||
return MESH_WRITE_CLASSES.includes(value as MeshWriteClass);
|
||||
}
|
||||
|
||||
export function isMeshIntentState(value: string): value is MeshIntentState {
|
||||
return MESH_INTENT_STATES.includes(value as MeshIntentState);
|
||||
}
|
||||
|
||||
export function getCoordinationModeForEntity(entityType: SharedMeshEntityType): SharedMeshCoordinationMode {
|
||||
return COORDINATION_BY_ENTITY[entityType];
|
||||
}
|
||||
|
||||
export function getDefaultWriteClassForEntity(entityType: SharedMeshEntityType): MeshWriteClass {
|
||||
const mode = getCoordinationModeForEntity(entityType);
|
||||
if (mode === "append-only-replicated") return "append-only";
|
||||
if (mode === "queued-for-later") return "queued";
|
||||
if (mode === "node-local-only") return "local";
|
||||
return "strong";
|
||||
}
|
||||
|
||||
export function getQuorumRequirement(eligibleVoters: number): SharedMeshQuorumRequirement {
|
||||
const normalized = Math.max(1, Math.floor(eligibleVoters));
|
||||
return {
|
||||
eligibleVoters: normalized,
|
||||
requiredAcks: Math.floor(normalized / 2) + 1,
|
||||
};
|
||||
}
|
||||
|
||||
export function isQuorumSatisfied(eligibleVoters: number, ackCount: number): boolean {
|
||||
return ackCount >= getQuorumRequirement(eligibleVoters).requiredAcks;
|
||||
}
|
||||
|
||||
export function createFenceToken(leaseEpoch: number, coordinatorNodeId: string, originSeq: number): string {
|
||||
return `${leaseEpoch}:${coordinatorNodeId}:${originSeq}`;
|
||||
}
|
||||
|
||||
export function isProtocolRef(value: { protocol?: string; version?: string } | null | undefined): value is SharedMeshProtocolRef {
|
||||
return value?.protocol === SHARED_MESH_PROTOCOL_ID && value.version === SHARED_MESH_PROTOCOL_VERSION;
|
||||
}
|
||||
|
||||
export function classifyReadStaleness(params: {
|
||||
queueDepth: number;
|
||||
lastGlobalCommitAt?: string;
|
||||
observedAt?: string;
|
||||
}): SharedMeshReadStaleness {
|
||||
const observedAt = params.observedAt ? Date.parse(params.observedAt) : Date.now();
|
||||
const lastGlobal = params.lastGlobalCommitAt ? Date.parse(params.lastGlobalCommitAt) : undefined;
|
||||
const lag = lastGlobal !== undefined && Number.isFinite(lastGlobal) ? Math.max(0, observedAt - lastGlobal) : undefined;
|
||||
const queueDepth = Math.max(0, params.queueDepth);
|
||||
|
||||
return {
|
||||
source: queueDepth > 0 ? "local-queued" : "local-committed",
|
||||
lastGlobalCommitAt: params.lastGlobalCommitAt,
|
||||
replicationLagMs: lag,
|
||||
queueDepth,
|
||||
isStale: queueDepth > 0 || (lag ?? 0) > 0,
|
||||
};
|
||||
}
|
||||
@@ -1251,7 +1251,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
/>
|
||||
<span className="text-secondary">min</span>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleCustomHeartbeatSave(agent)}
|
||||
disabled={isUpdatingHeartbeat}
|
||||
title="Save custom interval"
|
||||
@@ -1259,7 +1259,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
Save
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => {
|
||||
setCustomHeartbeatAgentId(null);
|
||||
setCustomHeartbeatMinutes((prev) => {
|
||||
@@ -1327,7 +1327,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
<div className="agent-card-actions">
|
||||
{agent.state === "idle" && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Activate"
|
||||
@@ -1338,7 +1338,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
{agent.state === "active" && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleRunHeartbeat(agent.id, agent.name)}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Run Now"
|
||||
@@ -1347,7 +1347,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
<Activity size={14} /> Run Now
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
@@ -1358,7 +1358,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
)}
|
||||
{agent.state === "paused" && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Resume"
|
||||
@@ -1369,7 +1369,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
{agent.state === "running" && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => openAgentDetail(agent.id, { initialTab: "runs", initialRunId: null, preferActiveRun: true })}
|
||||
title="View live run details"
|
||||
aria-label={`View live run details for ${agent.name}`}
|
||||
@@ -1377,7 +1377,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
<Activity size={14} /> Running
|
||||
</button>
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "paused")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Pause"
|
||||
@@ -1388,7 +1388,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
)}
|
||||
{agent.state === "error" && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Retry"
|
||||
@@ -1398,7 +1398,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
)}
|
||||
{agent.state === "terminated" && (
|
||||
<button
|
||||
className="btn btn--sm"
|
||||
className="btn btn-sm"
|
||||
onClick={() => void handleStateChange(agent.id, "active")}
|
||||
disabled={transitioningAgentIds.has(agent.id)}
|
||||
title="Start"
|
||||
@@ -1407,7 +1407,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="btn btn--sm agent-card-details-btn"
|
||||
className="btn btn-sm agent-card-details-btn"
|
||||
onClick={() => openAgentDetail(agent.id)}
|
||||
title={`View details for ${agent.name}`}
|
||||
aria-label={`View details for ${agent.name}`}
|
||||
@@ -1416,7 +1416,7 @@ export function AgentsView({ addToast, projectId, onOpenTaskLogs, agentOnboardin
|
||||
</button>
|
||||
{(agent.state === "idle" || agent.state === "terminated" || agent.state === "paused") && (
|
||||
<button
|
||||
className="btn btn--sm btn--danger"
|
||||
className="btn btn-sm btn-danger"
|
||||
onClick={() => void handleDelete(agent.id, agent.name)}
|
||||
title="Delete"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user