feat(agent-config): add typed dashboard mutation API

This commit is contained in:
vince 2026-06-19 12:12:16 +02:00
parent 8af0fce6f1
commit 9b7f20c486
5 changed files with 1668 additions and 6 deletions

View file

@ -1,6 +1,11 @@
export {
DASHBOARD_SCHEMA_VERSION,
DashboardDocumentSchema,
DatasourceReferenceSchema,
ServiceEntrySchema,
ServiceGroupSchema,
TelemetryCardSchema,
ThresholdSchema,
dashboardDocumentJsonSchema,
type DashboardDocument,
type DashboardModule,

View file

@ -27,7 +27,7 @@ const LinkSchema = Type.Object(
{ additionalProperties: false },
);
const StaticDatasourceSchema = Type.Object(
export const StaticDatasourceSchema = Type.Object(
{
type: Type.Literal("static"),
label: Type.Optional(Type.String({ minLength: 1 })),
@ -36,7 +36,7 @@ const StaticDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const PlaceholderDatasourceSchema = Type.Object(
export const PlaceholderDatasourceSchema = Type.Object(
{
type: Type.Literal("placeholder"),
reason: Type.String({ minLength: 1 }),
@ -44,7 +44,7 @@ const PlaceholderDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const ExternalDatasourceSchema = Type.Object(
export const ExternalDatasourceSchema = Type.Object(
{
type: Type.Literal("external"),
adapter: Type.Union([
@ -58,7 +58,7 @@ const ExternalDatasourceSchema = Type.Object(
{ additionalProperties: false },
);
const DatasourceReferenceSchema = Type.Union([
export const DatasourceReferenceSchema = Type.Union([
StaticDatasourceSchema,
PlaceholderDatasourceSchema,
ExternalDatasourceSchema,
@ -98,13 +98,13 @@ const TextMetricValueSchema = Type.Object(
{ additionalProperties: false },
);
const MetricValueSchema = Type.Union([
export const MetricValueSchema = Type.Union([
PercentMetricValueSchema,
NonNegativeMetricValueSchema,
TextMetricValueSchema,
]);
const ThresholdSchema = Type.Object(
export const ThresholdSchema = Type.Object(
{
warning: Type.Optional(Type.Number({ minimum: 0 })),
danger: Type.Optional(Type.Number({ minimum: 0 })),

View file

@ -0,0 +1,446 @@
import { existsSync, rmSync } from "node:fs";
import { mkdtemp } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, test } from "vitest";
import { genericDashboardFixture } from "$lib/model/fixtures/generic";
import { createDashboardStore, type DashboardStore } from "$lib/server/db/dashboard-store";
import {
AgentConfigAuthorizationError,
authorizeAgentConfigRequest,
handleAgentDashboardRequest,
previewDashboardChanges,
publishDashboardChanges,
rollbackDashboardRevision,
type AgentDashboardOperation,
} from ".";
const stores: DashboardStore[] = [];
const tempRoots: string[] = [];
afterEach(() => {
stores.splice(0).forEach((store) => store.close());
tempRoots.splice(0).forEach((path) => rmSync(path, { force: true, recursive: true }));
});
describe("agent dashboard configuration API", () => {
test("previews typed dashboard operations with an RFC 6902-compatible patch", () => {
const operations = exampleOperations();
const result = previewDashboardChanges(genericDashboardFixture, operations);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected preview success");
expect(result.document.serviceGroups[0]?.id).toBe("edge");
expect(result.document.serviceGroups[0]?.services[0]).toMatchObject({
id: "edge-router",
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://edge.example.test/api/status",
},
});
expect(result.document.layout.telemetry).toEqual([
"service-uptime",
"edge-latency",
"queue-depth",
]);
expect(genericDashboardFixture.serviceGroups.map((group) => group.id)).toEqual([
"core-services",
]);
expect(result.patch.length).toBeGreaterThan(0);
expect(result.patch.every((operation) => operation.path.startsWith("/"))).toBe(true);
expect(result.patch.map((operation) => operation.op)).toContain("add");
});
test("returns structured repairable errors for invalid operations", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "add_service",
groupId: "missing-group",
service: exampleService(),
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected preview failure");
expect(result.errors).toEqual([
expect.objectContaining({
code: "group_not_found",
operationIndex: 0,
path: "/serviceGroups",
}),
]);
});
test("rejects unsupported target-specific mutations", () => {
const datasourceResult = previewDashboardChanges(genericDashboardFixture, [
{
type: "connect_datasource",
target: { kind: "statusItem", stripId: "runtime", id: "status" },
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://status.example.test/api",
},
},
]);
expect(datasourceResult.ok).toBe(false);
if (datasourceResult.ok) throw new Error("expected datasource failure");
expect(datasourceResult.errors[0]).toMatchObject({
code: "unsupported_target",
operationIndex: 0,
path: "/statusStrips",
});
const thresholdResult = previewDashboardChanges(genericDashboardFixture, [
{
type: "set_status_rule",
target: { kind: "service", id: "identity" },
thresholds: { warning: 1 },
},
]);
expect(thresholdResult.ok).toBe(false);
if (thresholdResult.ok) throw new Error("expected threshold failure");
expect(thresholdResult.errors[0]).toMatchObject({
code: "unsupported_target",
operationIndex: 0,
path: "/serviceGroups",
});
});
test("removes status strip items through the shared remove operation", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "remove_item",
target: { kind: "statusItem", stripId: "runtime", id: "sync" },
},
]);
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected remove success");
expect(result.document.statusStrips[0]?.items.map((item) => item.id)).toEqual([
"status",
]);
expect(result.patch.map((operation) => operation.op)).toContain("remove");
});
test("rejects create_dashboard when it is not the first operation", () => {
const result = previewDashboardChanges(genericDashboardFixture, [
{
type: "remove_item",
target: { kind: "telemetry", id: "queue-depth" },
},
{
type: "create_dashboard",
document: genericDashboardFixture,
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected sequence failure");
expect(result.errors[0]).toMatchObject({
code: "invalid_operation_sequence",
operationIndex: 1,
path: "/1",
});
});
test("publishes valid operations as a persisted dashboard revision", async () => {
const { dbPath, store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
message: "initial dashboard",
});
const result = publishDashboardChanges(store, exampleOperations(), {
actor: "agent",
message: "add edge router",
});
expect(result.ok).toBe(true);
if (!result.ok) throw new Error("expected publish success");
expect(existsSync(dbPath)).toBe(true);
expect(result.revision.operation).toBe("commit");
expect(result.revision.actor).toBe("agent");
expect(result.revision.message).toBe("add edge router");
expect(result.previousRevisionId).toBe(seed.id);
expect(result.patch.length).toBeGreaterThan(0);
expect(store.getActiveDashboard()?.document.serviceGroups[0]?.id).toBe("edge");
expect(store.listRevisions()).toHaveLength(2);
});
test("does not publish invalid operations", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const result = publishDashboardChanges(store, [
{
type: "remove_item",
target: { kind: "telemetry", id: "missing-metric" },
},
]);
expect(result.ok).toBe(false);
if (result.ok) throw new Error("expected publish failure");
expect(result.errors[0]).toMatchObject({
code: "item_not_found",
operationIndex: 0,
});
expect(store.getActiveDashboard()?.currentRevisionId).toBe(seed.id);
expect(store.listRevisions()).toHaveLength(1);
});
test("rolls back through the agent-safe revision path", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const publish = publishDashboardChanges(store, exampleOperations(), {
actor: "agent",
});
expect(publish.ok).toBe(true);
const rollback = rollbackDashboardRevision(store, seed.id, {
actor: "agent",
message: "restore previous dashboard",
});
expect(rollback.operation).toBe("rollback");
expect(rollback.actor).toBe("agent");
expect(rollback.sourceRevisionId).toBe(seed.id);
expect(store.getActiveDashboard()?.document.metadata.title).toBe("Operations Console");
expect(store.listRevisions().map((revision) => revision.operation)).toEqual([
"rollback",
"commit",
"seed",
]);
});
test("authenticates agent configuration requests with a shared token", () => {
const authorized = new Request("https://dimensionlab.test/api/agent/dashboard", {
headers: { authorization: "Bearer shared-secret" },
});
const rejected = new Request("https://dimensionlab.test/api/agent/dashboard");
expect(authorizeAgentConfigRequest(authorized, "shared-secret")).toEqual({
ok: true,
});
expect(() => authorizeAgentConfigRequest(rejected, "shared-secret")).toThrow(
AgentConfigAuthorizationError,
);
expect(() => authorizeAgentConfigRequest(authorized, "")).toThrow(
AgentConfigAuthorizationError,
);
});
test("handles preview, publish, and rollback requests through the HTTP adapter", async () => {
const { store } = await createTestStore();
const seed = store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const preview = await handleAgentDashboardRequest(
jsonRequest({
action: "preview_changes",
operations: exampleOperations(),
}),
{ store, token: "shared-secret" },
);
expect(preview.status).toBe(200);
expect(await preview.json()).toMatchObject({
ok: true,
action: "preview_changes",
patch: expect.any(Array),
});
expect(store.listRevisions()).toHaveLength(1);
const publish = await handleAgentDashboardRequest(
jsonRequest({
action: "publish_changes",
actor: "agent",
message: "publish edge router",
operations: exampleOperations(),
}),
{ store, token: "shared-secret" },
);
expect(publish.status).toBe(200);
expect(await publish.json()).toMatchObject({
ok: true,
action: "publish_changes",
revision: { operation: "commit", actor: "agent" },
});
expect(store.listRevisions()).toHaveLength(2);
const rollback = await handleAgentDashboardRequest(
jsonRequest({
action: "rollback_revision",
actor: "agent",
revisionId: seed.id,
}),
{ store, token: "shared-secret" },
);
expect(rollback.status).toBe(200);
expect(await rollback.json()).toMatchObject({
ok: true,
action: "rollback_revision",
revision: { operation: "rollback", sourceRevisionId: seed.id },
});
});
test("returns a structured error for unknown rollback revisions", async () => {
const { store } = await createTestStore();
store.seedDashboardIfEmpty(genericDashboardFixture, {
actor: "seed",
});
const rollback = await handleAgentDashboardRequest(
jsonRequest({
action: "rollback_revision",
actor: "agent",
revisionId: "missing-revision",
}),
{ store, token: "shared-secret" },
);
expect(rollback.status).toBe(404);
expect(await rollback.json()).toMatchObject({
ok: false,
errors: [
{
code: "revision_not_found",
path: "/revisionId",
},
],
});
});
test("previews create_dashboard requests against an empty store", async () => {
const { store } = await createTestStore();
const preview = await handleAgentDashboardRequest(
jsonRequest({
action: "preview_changes",
operations: [
{
type: "create_dashboard",
document: genericDashboardFixture,
},
],
}),
{ store, token: "shared-secret" },
);
expect(preview.status).toBe(200);
expect(await preview.json()).toMatchObject({
ok: true,
action: "preview_changes",
document: {
metadata: {
title: "Operations Console",
},
},
});
expect(store.listRevisions()).toHaveLength(0);
});
});
async function createTestStore() {
const root = await mkdtemp(join(tmpdir(), "dimensionlab-agent-config-"));
tempRoots.push(root);
const dbPath = join(root, "dashboard.sqlite");
const store = createDashboardStore({
databaseUrl: `file:${dbPath}`,
});
stores.push(store);
return { dbPath, store };
}
function exampleOperations(): AgentDashboardOperation[] {
return [
{
type: "add_section",
section: {
id: "edge",
title: "Edge",
layout: "list",
},
},
{
type: "add_service",
groupId: "edge",
service: exampleService(),
},
{
type: "add_metric_card",
card: {
id: "edge-latency",
label: "Edge Latency",
value: { kind: "latency", value: 12, precision: 0 },
severity: "ok",
detail: "p95",
datasource: {
type: "external",
adapter: "prometheus",
reference: "histogram_quantile(0.95, edge_request_duration_seconds_bucket)",
},
},
position: { afterId: "service-uptime" },
},
{
type: "connect_datasource",
target: {
kind: "service",
groupId: "edge",
id: "edge-router",
},
datasource: {
type: "external",
adapter: "http-status",
reference: "GET https://edge.example.test/api/status",
},
},
{
type: "set_status_rule",
target: { kind: "telemetry", id: "edge-latency" },
thresholds: { warning: 50, danger: 100 },
},
{
type: "arrange_item",
area: "serviceGroups",
id: "edge",
index: 0,
},
];
}
function exampleService() {
return {
id: "edge-router",
label: "Edge Router",
description: "Ingress and routing",
icon: "mdi:router-network",
severity: "ok" as const,
detail: "pending datasource",
datasource: { type: "placeholder" as const, reason: "health adapter pending" },
link: {
href: "https://edge.example.test",
label: "Open Edge Router",
external: true,
},
};
}
function jsonRequest(body: unknown) {
return new Request("https://dimensionlab.test/api/agent/dashboard", {
body: JSON.stringify(body),
headers: {
authorization: "Bearer shared-secret",
"content-type": "application/json",
},
method: "POST",
});
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,4 @@
import { handleAgentDashboardRequest } from "$lib/server/agent-config";
import type { RequestHandler } from "./$types";
export const POST: RequestHandler = ({ request }) => handleAgentDashboardRequest(request);