starling-devex 0.1.2

Starling: a local dev orchestrator with a central daemon, shared named-URL proxy, and a k9s-style TUI (a Rust port of Tilt + portless)
import { Hold } from "./Hold"
import LogStore, { LogAlert, LogAlertIndex } from "./LogStore"
import {
  buildStatus,
  combinedStatus,
  PendingBuildDescription,
  runtimeStatus,
} from "./status"
import { oneResource } from "./testdata"
import { zeroTime } from "./time"
import { LogLevel, ResourceStatus, RuntimeStatus, UpdateStatus } from "./types"

class FakeAlertIndex implements LogAlertIndex {
  alerts: { [key: string]: LogAlert[] } = {}

  alertsForSpanId(spanId: string): LogAlert[] {
    return this.alerts[spanId] || []
  }
}

function emptyResource() {
  let res = oneResource({})
  res.status!.currentBuild = { startTime: zeroTime }
  res.status!.buildHistory = []
  res.status!.pendingBuildSince = zeroTime
  res.status!.runtimeStatus = "pending"
  res.status!.updateStatus = "none"
  return res
}

describe("combinedStatus", () => {
  it("pending when no build info", () => {
    let ls = new LogStore()
    let res = emptyResource()
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Pending
    )
  })

  it("building when current build", () => {
    let ls = new LogStore()
    const ts = Date.now().toLocaleString()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.InProgress
    res.status!.runtimeStatus = RuntimeStatus.Ok
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Building
    )
  })

  it("healthy when runtime ok", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.Ok
    res.status!.runtimeStatus = RuntimeStatus.Ok
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Healthy
    )
  })

  it("unhealthy when runtime error", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.Ok
    res.status!.runtimeStatus = RuntimeStatus.Error
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Unhealthy
    )
  })

  it("unhealthy when last build error", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.Error
    res.status!.runtimeStatus = RuntimeStatus.Ok
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Unhealthy
    )
  })

  it("building when runtime status error, but also building", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.InProgress
    res.status!.runtimeStatus = RuntimeStatus.Error
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Building
    )
  })

  it("unhealthy when warning and runtime error", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.runtimeStatus = RuntimeStatus.Error
    if (!res.status!.k8sResourceInfo) throw new Error("missing k8s info")
    res.status!.k8sResourceInfo.podRestarts = 1
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Unhealthy
    )
  })

  it("warning when container restarts", () => {
    let ls = new FakeAlertIndex()
    ls.alerts["pod-span-id"] = [{ level: LogLevel.WARN, lineIndex: 1 }]
    const ts = Date.now().toLocaleString()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.Ok
    res.status!.runtimeStatus = RuntimeStatus.Ok
    if (!res.status!.k8sResourceInfo) throw new Error("missing k8s info")
    res.status!.k8sResourceInfo.podRestarts = 1
    res.status!.k8sResourceInfo.spanID = "pod-span-id"
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Warning
    )
  })

  it("none when n/a runtime status and no builds", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.updateStatus = UpdateStatus.None
    res.status!.runtimeStatus = RuntimeStatus.NotApplicable
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.None
    )
  })

  it("healthy when n/a runtime status and last build succeeded", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.runtimeStatus = RuntimeStatus.NotApplicable
    res.status!.updateStatus = UpdateStatus.Ok
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Healthy
    )
  })

  it("unhealthy when n/a runtime status and last build failed", () => {
    let ls = new LogStore()
    let res = emptyResource()
    res.status!.runtimeStatus = RuntimeStatus.NotApplicable
    res.status!.updateStatus = UpdateStatus.Error
    expect(combinedStatus(buildStatus(res, ls), runtimeStatus(res, ls))).toBe(
      ResourceStatus.Unhealthy
    )
  })
})

describe("PendingBuildDescription", () => {
  it("shows a generic message if there is no hold", () => {
    expect(PendingBuildDescription(null)).toBe("Update: pending")
  })

  it("shows a generic message if there are no dependencies", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [],
    })
    expect(PendingBuildDescription(hold)).toBe("Update: pending")
  })

  it("shows single image name", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [{ group: "", apiVersion: "", kind: "ImageMap", name: "gcr.io/foo" }],
    })
    expect(PendingBuildDescription(hold)).toBe(
      "Update: waiting on image: gcr.io/foo"
    )
  })

  it("shows single resource name", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [{ group: "", apiVersion: "", kind: "UIResource", name: "bar" }],
    })
    expect(PendingBuildDescription(hold)).toBe(
      "Update: waiting on resource: bar"
    )
  })

  it("shows multiple resource names without overflow", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [
        { group: "", apiVersion: "", kind: "UIResource", name: "foo" },
        { group: "", apiVersion: "", kind: "UIResource", name: "bar" },
        { group: "", apiVersion: "", kind: "UIResource", name: "baz" },
      ],
    })
    expect(PendingBuildDescription(hold)).toBe(
      "Update: waiting on resources: foo, bar, baz"
    )
  })

  it("shows multiple image names with overflow", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: ["a", "b", "c", "d", "e"].map((x) => ({
        group: "",
        apiVersion: "",
        kind: "ImageMap",
        name: x,
      })),
    })
    expect(PendingBuildDescription(hold)).toBe(
      "Update: waiting on images: a, b, c, and 2 more"
    )
  })

  it("shows cluster name", () => {
    let hold = new Hold({
      reason: "waiting-for-cluster",
      on: [{ group: "", apiVersion: "", kind: "Cluster", name: "default" }],
    })
    expect(PendingBuildDescription(hold)).toBe(
      "Update: waiting on cluster: default"
    )
  })

  it("prefers image over resource", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [
        { group: "", apiVersion: "", kind: "UIResource", name: "foo" },
        { group: "", apiVersion: "", kind: "ImageMap", name: "bar" },
      ],
    })
    expect(PendingBuildDescription(hold)).toBe("Update: waiting on image: bar")
  })

  it("gracefully falls back for unknown types", () => {
    let hold = new Hold({
      reason: "waiting-for-deploy",
      on: [
        { group: "", apiVersion: "", kind: "ThisIsNotARealKind", name: "foo" },
      ],
    })
    expect(PendingBuildDescription(hold)).toBe("Update: waiting on 1 object")
  })
})