Files
legion_kk/tests/unit/app-controller.test.ts
self a4eddb1673 feat: add dev log generator
Add a dev-only workspace tab opened with Cmd/Ctrl+Shift+O for manual UI testing.

Wire the log generator through the typed preload and main IPC path so entries are appended by the real log store.

Translate the dev UI and cover the controller API handoff in unit tests.
2026-06-17 19:21:10 +02:00

632 lines
18 KiB
TypeScript

import assert from "node:assert/strict"
import test from "node:test"
import type {
AppSnapshot,
ObservationSnapshot,
ProductCatalog,
ProviderDescriptor
} from "../../src/shared/app"
import { createProviderConfigRequest } from "../../src/renderer/src/state/settings-actions"
import { createServerForm } from "../../src/renderer/src/forms/server-form"
import { getRendererApi, type RendererApi } from "../../src/renderer/src/state/api"
import { createEditorController } from "../../src/renderer/src/state/controllers/editor-controller"
import { createSchemeController } from "../../src/renderer/src/state/controllers/scheme-controller"
import { createAppController } from "../../src/renderer/src/state/app-controller.svelte"
import { toAppError } from "../../src/renderer/src/state/api-errors"
import type { ControllerContext } from "../../src/renderer/src/state/controller-types"
import { operationFailed, operationSucceeded } from "../../src/renderer/src/state/results"
import { createInitialAppModel, type AppModel } from "../../src/renderer/src/state/model"
import { syncSettingsDrafts } from "../../src/renderer/src/state/view-flows"
function createSnapshot(): AppSnapshot {
return {
profile: {
ownerName: "Ada",
domain: "example.org",
useExistingNostrAccount: true,
nostrPrivateKey: "nsec-example"
},
providers: [
{
id: "hetzner",
kind: "hetzner",
credentials: {
kind: "hetzner",
apiKey: "token-primary"
},
capabilities: {
computeCatalog: true,
computeProvisioning: true,
computeReinstallFromImage: false,
domainRegistration: false,
standardDnsService: true,
fallbackDnsService: false,
dnsRecordManagement: true,
firewallManagement: true
},
configured: true,
createdAt: "2026-03-20T00:00:00.000Z"
},
{
id: "hetzner-secondary",
kind: "hetzner",
credentials: {
kind: "hetzner",
apiKey: "token-secondary"
},
capabilities: {
computeCatalog: true,
computeProvisioning: true,
computeReinstallFromImage: false,
domainRegistration: false,
standardDnsService: true,
fallbackDnsService: false,
dnsRecordManagement: true,
firewallManagement: true
},
configured: true,
createdAt: "2026-03-20T00:00:00.000Z"
}
],
scheme: {
servers: [],
domains: [],
dnsHosts: [],
dnsZones: []
},
instances: {
servers: [],
domains: [],
dnsHosts: [],
dnsZones: []
},
plannedChanges: [],
isConfigured: true,
tutorial: { completed: false },
hasPendingChanges: false
}
}
function createCatalog(planName: string, providerKind: "hetzner" = "hetzner"): ProductCatalog {
return {
providerKind,
isDefaultProvider: true,
defaultProductId: `${planName}-id`,
products: [
{
id: `${planName}-id`,
providerKind,
providerProductId: `${planName}-remote`,
label: planName,
planName,
description: `${planName} description`,
architecture: "x86_64",
category: "general",
cores: 2,
cpuType: "shared",
memoryGb: 4,
diskGb: 40,
storageType: "ssd",
image: "ubuntu-24.04",
bootMode: "bios",
defaultRegion: "nbg1",
availableRegions: ["nbg1"],
defaultPrice: {
region: "nbg1",
hourlyGrossEur: 0.01,
hourlyNetEur: 0.01,
monthlyGrossEur: 5,
monthlyNetEur: 5
},
prices: [
{
region: "nbg1",
hourlyGrossEur: 0.01,
hourlyNetEur: 0.01,
monthlyGrossEur: 5,
monthlyNetEur: 5
}
],
commercialMetadata: {
effectiveMonthlyGrossEur: 5,
source: "provider",
notes: []
},
recommended: true
}
],
generatedAt: "2026-03-20T00:00:00.000Z",
disclaimer: "test"
}
}
function createProviderDescriptors(): ProviderDescriptor[] {
return [
{
kind: "hetzner",
label: "Hetzner Cloud",
description: "Hetzner",
defaultProviderId: "hetzner",
capabilities: {
computeCatalog: true,
computeProvisioning: true,
computeReinstallFromImage: false,
domainRegistration: false,
standardDnsService: true,
fallbackDnsService: false,
dnsRecordManagement: true,
firewallManagement: true
},
credentialFields: [
{
id: "apiKey",
label: "API token",
required: true,
secret: true
}
]
},
{
kind: "scaleway",
label: "Scaleway",
description: "Scaleway",
defaultProviderId: "scaleway",
capabilities: {
computeCatalog: true,
computeProvisioning: true,
computeReinstallFromImage: false,
domainRegistration: false,
standardDnsService: false,
fallbackDnsService: false,
dnsRecordManagement: false,
firewallManagement: true
},
credentialFields: [
{ id: "accessKey", label: "Access key", required: true, secret: true },
{ id: "secretKey", label: "Secret key", required: true, secret: true },
{ id: "projectId", label: "Project ID", required: true }
]
}
]
}
function createObservationSnapshot(): ObservationSnapshot {
return {
generatedAt: "2026-03-20T00:00:00.000Z",
cloud: { status: "idle" },
catalogs: { status: "idle" },
telemetry: { status: "idle" },
metrics: {
rollups: []
},
logs: {
status: "idle"
},
lanes: []
}
}
function deferred<T>(): {
promise: Promise<T>
resolve(value: T): void
} {
let resolve!: (value: T) => void
const promise = new Promise<T>((nextResolve) => {
resolve = nextResolve
})
return { promise, resolve }
}
function createTestApi(overrides: Partial<RendererApi>): RendererApi {
const noopCleanup = (): void => {}
const unexpected = async (): Promise<never> => {
throw new Error("Unexpected API call")
}
return {
bootstrap: async () => ({
snapshot: createSnapshot(),
activity: null,
auth: {
locked: false,
availableMethods: ["password"]
}
}),
getProviderDescriptors: async () => createProviderDescriptors(),
getProductCatalog: unexpected,
getDnsProductCatalog: unexpected,
getDnsRecordSets: unexpected,
upsertDnsRecordSet: unexpected,
deleteDnsRecordSet: unexpected,
verifyProviderCredentials: unexpected,
refreshProviderCatalogs: unexpected,
getProvisioningQuote: unexpected,
findNodeOffers: unexpected,
getObservationSnapshot: async () => createObservationSnapshot(),
getObservationMetrics: unexpected,
acknowledgeObservationLogs: async () => createObservationSnapshot(),
listLogEntries: unexpected,
unlock: unexpected,
logout: unexpected,
materialize: unexpected,
upsertServerPlan: unexpected,
removeServerPlan: unexpected,
upsertDnsZonePlan: unexpected,
removeDnsZonePlan: unexpected,
upsertDnsRecordPlan: unexpected,
removeDnsRecordPlan: unexpected,
openServerTerminal: unexpected,
openServerTerminalWindow: unexpected,
writeTerminal: unexpected,
resizeTerminal: unexpected,
closeTerminal: unexpected,
updateConfiguration: unexpected,
updateProfile: unexpected,
upsertProviderConfig: unexpected,
upsertDomainPlan: unexpected,
removeDomainPlan: unexpected,
getPlannedDomainNameServers: unexpected,
quoteDomain: unexpected,
updateScheme: unexpected,
resetScheme: unexpected,
updateTutorial: unexpected,
performServerAction: unexpected,
startSenderStream: unexpected,
stopSenderStream: unexpected,
getSenderStreamStatus: unexpected,
generateDevLogEntry: unexpected,
exportDatabaseDump: unexpected,
onSnapshot: () => noopCleanup,
onActivity: () => noopCleanup,
onTerminalData: () => noopCleanup,
onTerminalExit: () => noopCleanup,
onCatalogUpdate: () => noopCleanup,
onObservationStatus: () => noopCleanup,
onObservationMetrics: () => noopCleanup,
onObservationLogs: () => noopCleanup,
...overrides
} as RendererApi
}
function createHarness(api: RendererApi): {
getModel(): AppModel
setModel(nextModel: AppModel): void
context: ControllerContext
} {
const t = (id: string): string => id
let model = createInitialAppModel()
function updateModel(update: (current: AppModel) => AppModel): void {
model = update(model)
}
const context: ControllerContext = {
getModel: () => model,
updateModel,
api: () => api,
async runOperation(run) {
updateModel((current) => ({
...current,
busy: true
}))
try {
await run()
updateModel((current) => ({
...current,
loading: false,
busy: false
}))
return operationSucceeded()
} catch (error) {
updateModel((current) => ({
...current,
loading: false,
busy: false
}))
return operationFailed(toAppError(error, t("feedback.requestedActionFailed")).message)
}
},
t
}
return {
getModel: () => model,
setModel(nextModel) {
model = nextModel
},
context
}
}
test("scheme controller profile save invokes the mutation without applying returned state", async () => {
let profileOwnerName: string | null = null
const harness = createHarness(
createTestApi({
updateProfile: async (request) => {
profileOwnerName = request.ownerName
}
})
)
const controller = createSchemeController(harness.context)
const result = await controller.saveProfile({
ownerName: "Ada",
domain: "example.org",
useExistingNostrAccount: true,
nostrPrivateKey: "nsec-example"
})
assert.equal(result.ok, true)
assert.equal(profileOwnerName, "Ada")
assert.equal(harness.getModel().snapshot, null)
assert.equal(harness.getModel().busy, false)
})
test("settings sync seeds drafts from the current snapshot and provider descriptors", () => {
const model = {
...createInitialAppModel(),
snapshot: createSnapshot(),
providerDescriptors: createProviderDescriptors()
}
const nextModel = syncSettingsDrafts(model, model.snapshot)
assert.equal(nextModel.settings.profileForm.ownerName, "Ada")
assert.equal(nextModel.settings.providerForms.hetzner?.values.apiKey, "token-primary")
assert.equal(nextModel.settings.profileForm.domain, "example.org")
})
test("app controller generates dev log entries through the renderer api", async () => {
const globalWithRunes = globalThis as Record<string, unknown>
const previousStateRune = globalWithRunes["$state"]
globalWithRunes["$state"] = <T>(value: T): T => value
let generatedRequest: Parameters<RendererApi["generateDevLogEntry"]>[0] | null = null
const api = createTestApi({
generateDevLogEntry: async (request) => {
generatedRequest = request
}
})
const controller = createAppController({ api })
try {
const result = await controller.generateDevLogEntry({
severity: "error",
notifyUser: true,
message: "manual test"
})
assert.equal(result.ok, true)
assert.deepEqual(generatedRequest, {
severity: "error",
notifyUser: true,
message: "manual test"
})
} finally {
globalWithRunes["$state"] = previousStateRune
}
})
test("app controller applies server plan changes from snapshot events without refetching bootstrap", async () => {
const globalWithRunes = globalThis as Record<string, unknown>
const previousStateRune = globalWithRunes["$state"]
globalWithRunes["$state"] = <T>(value: T): T => value
let bootstrapCalls = 0
let snapshotHandler: ((snapshot: AppSnapshot | null) => void) | null = null
const nextSnapshot = {
...createSnapshot(),
scheme: {
...createSnapshot().scheme,
servers: [
{
id: "node-1",
label: "Primary",
flavor: "cluster",
providerKind: "hetzner",
providerId: "hetzner",
source: "catalog",
instanceId: "cx22-id",
planName: "cx22",
region: "nbg1",
commercialMetadata: {
effectiveMonthlyGrossEur: 5,
source: "provider",
notes: []
},
cores: 2,
memoryGb: 4,
diskGb: 40,
bandwidthMbps: 0,
role: "primary"
}
]
}
} satisfies AppSnapshot
const api = createTestApi({
bootstrap: async () => {
bootstrapCalls += 1
return {
snapshot: createSnapshot(),
activity: null,
auth: {
locked: false,
availableMethods: ["password"]
}
}
},
onSnapshot: (callback) => {
snapshotHandler = callback
return () => undefined
},
upsertServerPlan: async () => {
snapshotHandler?.(nextSnapshot)
}
})
const controller = createAppController({ api })
try {
await controller.initialize()
const form = {
...createServerForm({
snapshot: controller.model.snapshot,
providerDescriptors: createProviderDescriptors(),
t: (id: string) => id
}),
id: "node-1",
label: "Primary",
providerId: "hetzner",
selectedProductId: "cx22-id",
region: "nbg1",
cores: 2,
memoryGb: 4,
diskGb: 40
}
const result = await controller.saveServerPlan(form)
assert.equal(result.ok, true)
assert.equal(bootstrapCalls, 1)
assert.equal(controller.model.snapshot?.scheme.servers[0]?.id, "node-1")
controller.destroy()
} finally {
globalWithRunes["$state"] = previousStateRune
}
})
test("Scaleway provider drafts build provider credential upsert requests", () => {
const descriptor = createProviderDescriptors().find((entry) => entry.kind === "scaleway")
assert.ok(descriptor)
const request = createProviderConfigRequest(
{
kind: "scaleway",
values: {
accessKey: " access ",
secretKey: " secret ",
projectId: " project-1 "
}
},
descriptor
)
assert.equal(request.kind, "scaleway")
assert.equal(request.projectId, "project-1")
assert.deepEqual(request.credentials, {
kind: "scaleway",
accessKey: "access",
secretKey: "secret",
projectId: "project-1"
})
})
test("materialize failures preserve the underlying error message and settle busy state", async () => {
const api = createTestApi({
materialize: async () => {
throw new Error("Manual server planning requires public IP, SSH username, and password.")
}
})
const harness = createHarness(api)
const controller = createSchemeController(harness.context)
const result = await controller.materialize()
assert.equal(result.ok, false)
assert.equal(
result.errorMessage,
"Manual server planning requires public IP, SSH username, and password."
)
assert.equal(harness.getModel().busy, false)
})
test("editor controller refresh ignores stale async catalog responses and preserves the latest editor state", async () => {
const primaryCatalog = deferred<ProductCatalog>()
const secondaryCatalog = deferred<ProductCatalog>()
const catalogRequests: Array<Parameters<RendererApi["getProductCatalog"]>[0]> = []
const api = createTestApi({
getProductCatalog: async (request) => {
catalogRequests.push(request)
if (request.credentials?.kind !== "hetzner") {
throw new Error("Unexpected credentials")
}
if (request.credentials.apiKey === "token-primary") {
return await primaryCatalog.promise
}
if (request.credentials.apiKey === "token-secondary") {
return await secondaryCatalog.promise
}
throw new Error("Unknown provider")
}
})
const harness = createHarness(api)
const schemeController = createSchemeController(harness.context)
harness.setModel({
...harness.getModel(),
snapshot: createSnapshot(),
loading: false
})
const editorController = createEditorController(harness.context, {
saveServerPlan: async () => operationFailed(),
saveDomainPlan: async () => operationFailed(),
saveZonePlan: async () => operationFailed(),
saveDnsRecord: async () => operationFailed(),
loadServerCatalog: schemeController.loadServerCatalog,
loadDnsCatalog: schemeController.loadDnsCatalog
})
const initialForm = createServerForm({
snapshot: harness.getModel().snapshot,
t: (id) => id
})
editorController.updateServerEditor(initialForm)
const firstRefresh = editorController.refreshServerEditorCatalog(initialForm)
const updatedForm = {
...initialForm,
label: "Edited label",
providerId: "hetzner-secondary"
}
editorController.updateServerEditor(updatedForm)
const secondRefresh = editorController.refreshServerEditorCatalog(updatedForm)
secondaryCatalog.resolve(createCatalog("cx22-secondary"))
await secondRefresh
assert.equal(catalogRequests[0]?.serverFlavor, undefined)
assert.equal(catalogRequests[1]?.serverFlavor, undefined)
assert.equal(harness.getModel().editors.server?.selectedProductId, "cx22-secondary-id")
assert.equal(harness.getModel().editors.server?.label, "Edited label")
assert.equal(harness.getModel().catalogs.server.catalog?.defaultProductId, "cx22-secondary-id")
primaryCatalog.resolve(createCatalog("cx11-primary"))
await firstRefresh
assert.equal(harness.getModel().editors.server?.selectedProductId, "cx22-secondary-id")
assert.equal(harness.getModel().editors.server?.label, "Edited label")
assert.equal(harness.getModel().catalogs.server.catalog?.defaultProductId, "cx22-secondary-id")
})
test("renderer api lookup fails fast when the preload bridge did not expose window.api", () => {
const globalWithWindow = globalThis as typeof globalThis & {
window?: (Window & typeof globalThis) | undefined
}
const originalWindow = globalWithWindow.window
globalWithWindow.window = {} as Window & typeof globalThis
try {
assert.throws(
() => getRendererApi(),
/Renderer API is unavailable\. The preload bridge did not expose window\.api\./
)
} finally {
globalWithWindow.window = originalWindow
}
})