refactor(web): move website into turbo app workspace
This commit is contained in:
parent
2664804e91
commit
b4e626a868
66 changed files with 318 additions and 298 deletions
595
apps/web/src/lib/server/agent-config/agent-config.test.ts
Normal file
595
apps/web/src/lib/server/agent-config/agent-config.test.ts
Normal file
|
|
@ -0,0 +1,595 @@
|
|||
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 type { DashboardDocument } from "$lib/model";
|
||||
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,
|
||||
type JsonPatchOperation,
|
||||
} 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("returns RFC 6902-applicable patches for multiple array removals", () => {
|
||||
const result = previewDashboardChanges(genericDashboardFixture, [
|
||||
{
|
||||
type: "remove_item",
|
||||
target: { kind: "statusItem", stripId: "runtime", id: "status" },
|
||||
},
|
||||
{
|
||||
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.patch
|
||||
.filter((operation) => operation.op === "remove")
|
||||
.map((operation) => operation.path),
|
||||
).toEqual(["/statusStrips/0/items/1", "/statusStrips/0/items/0"]);
|
||||
expect(applyJsonPatch(genericDashboardFixture, result.patch)).toMatchObject({
|
||||
statusStrips: [{ items: [] }],
|
||||
});
|
||||
});
|
||||
|
||||
test("rejects ambiguous service and status targets", () => {
|
||||
const duplicateDocument = documentWithDuplicateNestedIds();
|
||||
|
||||
const connect = previewDashboardChanges(duplicateDocument, [
|
||||
{
|
||||
type: "connect_datasource",
|
||||
target: { kind: "service", id: "identity" },
|
||||
datasource: {
|
||||
type: "external",
|
||||
adapter: "http-status",
|
||||
reference: "GET https://identity.example.test/health",
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(connect.ok).toBe(false);
|
||||
if (connect.ok) throw new Error("expected ambiguous service failure");
|
||||
expect(connect.errors[0]).toMatchObject({
|
||||
code: "ambiguous_target",
|
||||
operationIndex: 0,
|
||||
path: "/serviceGroups",
|
||||
});
|
||||
|
||||
const remove = previewDashboardChanges(duplicateDocument, [
|
||||
{
|
||||
type: "remove_item",
|
||||
target: { kind: "service", id: "identity" },
|
||||
},
|
||||
]);
|
||||
expect(remove.ok).toBe(false);
|
||||
if (remove.ok) throw new Error("expected ambiguous removal failure");
|
||||
expect(remove.errors[0]).toMatchObject({
|
||||
code: "ambiguous_target",
|
||||
operationIndex: 0,
|
||||
path: "/serviceGroups",
|
||||
});
|
||||
|
||||
const status = previewDashboardChanges(duplicateDocument, [
|
||||
{
|
||||
type: "set_status_rule",
|
||||
target: { kind: "statusItem", id: "status" },
|
||||
value: "Healthy",
|
||||
},
|
||||
]);
|
||||
expect(status.ok).toBe(false);
|
||||
if (status.ok) throw new Error("expected ambiguous status failure");
|
||||
expect(status.errors[0]).toMatchObject({
|
||||
code: "ambiguous_target",
|
||||
operationIndex: 0,
|
||||
path: "/statusStrips",
|
||||
});
|
||||
});
|
||||
|
||||
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 documentWithDuplicateNestedIds(): DashboardDocument {
|
||||
const document = structuredClone(genericDashboardFixture);
|
||||
document.layout.serviceGroups.push("secondary-services");
|
||||
document.serviceGroups.push({
|
||||
id: "secondary-services",
|
||||
title: "Secondary Services",
|
||||
layout: "list",
|
||||
services: [
|
||||
{
|
||||
...document.serviceGroups[0].services[0],
|
||||
label: "Shadow Identity",
|
||||
},
|
||||
],
|
||||
});
|
||||
document.layout.statusStrips.push("secondary-runtime");
|
||||
document.statusStrips.push({
|
||||
id: "secondary-runtime",
|
||||
items: [
|
||||
{
|
||||
...document.statusStrips[0].items[0],
|
||||
value: "Healthy",
|
||||
},
|
||||
],
|
||||
});
|
||||
return document;
|
||||
}
|
||||
|
||||
function applyJsonPatch<T>(value: T, patch: JsonPatchOperation[]): T {
|
||||
const next = structuredClone(value);
|
||||
for (const operation of patch) {
|
||||
const { parent, key } = jsonPointerTarget(next, operation.path);
|
||||
if (operation.op === "remove") {
|
||||
if (Array.isArray(parent)) {
|
||||
parent.splice(Number(key), 1);
|
||||
} else {
|
||||
delete parent[key];
|
||||
}
|
||||
} else if (operation.op === "add") {
|
||||
if (Array.isArray(parent)) {
|
||||
parent.splice(Number(key), 0, operation.value);
|
||||
} else {
|
||||
parent[key] = operation.value;
|
||||
}
|
||||
} else if (operation.op === "replace") {
|
||||
if (Array.isArray(parent)) {
|
||||
parent[Number(key)] = operation.value;
|
||||
} else {
|
||||
parent[key] = operation.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
function jsonPointerTarget(value: unknown, path: string) {
|
||||
const segments = path
|
||||
.split("/")
|
||||
.slice(1)
|
||||
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
||||
const key = segments.pop();
|
||||
if (key === undefined) throw new Error(`Invalid JSON pointer: ${path}`);
|
||||
|
||||
let parent = value as Record<string, unknown> | unknown[];
|
||||
for (const segment of segments) {
|
||||
parent = Array.isArray(parent)
|
||||
? (parent[Number(segment)] as Record<string, unknown> | unknown[])
|
||||
: (parent[segment] as Record<string, unknown> | unknown[]);
|
||||
}
|
||||
return { parent, key };
|
||||
}
|
||||
|
||||
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",
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue