Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
687 lines
27 KiB
JavaScript
687 lines
27 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { collectStatus } from '../lib/status.mjs';
|
|
|
|
class MockCaptureRunner {
|
|
constructor(responses) {
|
|
this.responses = responses;
|
|
}
|
|
|
|
async runCapture(command, args) {
|
|
const key = `${command} ${args.join(' ')}`;
|
|
const response = this.responses[key];
|
|
if (!response) {
|
|
return { ok: false, code: 1, output: 'missing mock response' };
|
|
}
|
|
if (response.ok) {
|
|
return { ok: true, code: 0, output: response.output ?? '' };
|
|
}
|
|
return { ok: false, code: response.code ?? 1, output: response.output ?? '' };
|
|
}
|
|
}
|
|
|
|
function buildEnv() {
|
|
return {
|
|
runtime: { assetRoot: '/tmp' },
|
|
site: { siteId: 'appliance-single-node', configDir: '/tmp/config' },
|
|
paths: { kubeconfig: '/tmp/kubeconfig', talosconfig: '/tmp/talosconfig' },
|
|
nodeIp: '10.0.0.2',
|
|
appUrl: 'https://psa.example.com',
|
|
};
|
|
}
|
|
|
|
function readyCondition(status = 'True', message = '') {
|
|
return [{ type: 'Ready', status, message }];
|
|
}
|
|
|
|
function healthyResponses() {
|
|
const responses = {
|
|
'talosctl --talosconfig /tmp/talosconfig -n 10.0.0.2 -e 10.0.0.2 health --wait-timeout 20s': { ok: true, output: 'ok' },
|
|
'kubectl --kubeconfig /tmp/kubeconfig get --raw=/readyz': { ok: true, output: 'ok' },
|
|
'kubectl --kubeconfig /tmp/kubeconfig get nodes -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [{ metadata: { name: 'node-1' }, status: { conditions: readyCondition('True') } }],
|
|
}),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n flux-system get gitrepositories.source.toolkit.fluxcd.io -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { name: 'alga-appliance' },
|
|
status: { conditions: readyCondition('True'), artifact: { revision: 'sha1:1234abcd' } },
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n flux-system get kustomizations.kustomize.toolkit.fluxcd.io -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{ metadata: { name: 'alga-platform' }, status: { conditions: readyCondition('True') } },
|
|
{ metadata: { name: 'alga-core' }, status: { conditions: readyCondition('True') } },
|
|
{ metadata: { name: 'alga-background' }, status: { conditions: readyCondition('True') } },
|
|
],
|
|
}),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n alga-system get helmreleases.helm.toolkit.fluxcd.io -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({ items: [{ metadata: { name: 'alga-core' }, status: { conditions: readyCondition('True') } }] }),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n alga-system get configmap/appliance-release-selection -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({ data: { releaseVersion: '1.0.0', selectedChannel: 'stable', appVersion: '1.0.0', manifestDigest: 'sha256:release' } }),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n alga-system get configmap/appliance-values-alga-core -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({ data: { 'alga-core.single-node.yaml': 'appUrl: https://psa.example.com' } }),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig -n msp get jobs.batch -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({ items: [] }),
|
|
},
|
|
'kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json': {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Pulled',
|
|
type: 'Normal',
|
|
message: 'Successfully pulled image',
|
|
involvedObject: { kind: 'Pod', name: 'alga-core-sebastian-abc' },
|
|
},
|
|
],
|
|
}),
|
|
},
|
|
'curl -ksS -I --max-time 10 https://psa.example.com': {
|
|
ok: true,
|
|
output: 'HTTP/2 302\nlocation: https://psa.example.com/msp/dashboard\n',
|
|
},
|
|
};
|
|
|
|
const workloadResources = [
|
|
'deployment/alga-core-sebastian',
|
|
'statefulset/db',
|
|
'statefulset/redis',
|
|
'deployment/pgbouncer',
|
|
'deployment/temporal',
|
|
'deployment/workflow-worker',
|
|
'deployment/email-service',
|
|
'deployment/temporal-worker',
|
|
];
|
|
for (const resource of workloadResources) {
|
|
responses[`kubectl --kubeconfig /tmp/kubeconfig -n msp get ${resource} -o json`] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 1 } }),
|
|
};
|
|
}
|
|
return responses;
|
|
}
|
|
|
|
test('T001: canonical status shape includes release, urls, rollup, tiers, blockers, components, and events', async () => {
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(healthyResponses()),
|
|
});
|
|
|
|
assert.equal(status.canonical.siteId, 'appliance-single-node');
|
|
assert.equal(status.canonical.release.selectedReleaseVersion, '1.0.0');
|
|
assert.equal(status.canonical.release.appVersion, '1.0.0');
|
|
assert.equal(status.canonical.release.gitRevision, 'sha1:1234abcd');
|
|
assert.equal(status.canonical.urls.loginUrl, 'https://psa.example.com');
|
|
assert.equal(status.canonical.urls.statusUrl, 'http://10.0.0.2:8080');
|
|
assert.equal(status.canonical.rollup.state, 'fully_healthy');
|
|
assert.equal(status.canonical.tiers.platform.ready, true);
|
|
assert.equal(status.canonical.tiers.login.ready, true);
|
|
assert.equal(status.canonical.topBlockers.length, 0);
|
|
assert.equal(status.canonical.components.length, 8);
|
|
assert.equal(status.canonical.recentEvents.length, 1);
|
|
assert.equal(status.canonical.loginProbe.reachable, true);
|
|
});
|
|
|
|
test('T002: login-ready with background failures rolls up as ready_with_background_issues', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/workflow-worker -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n flux-system get kustomizations.kustomize.toolkit.fluxcd.io -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{ metadata: { name: 'alga-platform' }, status: { conditions: readyCondition('True') } },
|
|
{ metadata: { name: 'alga-core' }, status: { conditions: readyCondition('True') } },
|
|
{ metadata: { name: 'alga-background' }, status: { conditions: readyCondition('False', 'dependency not ready') } },
|
|
],
|
|
}),
|
|
};
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.canonical.tiers.login.ready, true);
|
|
assert.equal(status.canonical.tiers.background.ready, false);
|
|
assert.equal(status.canonical.rollup.state, 'ready_with_background_issues');
|
|
assert.notEqual(status.canonical.rollup.state, 'failed_action_required');
|
|
});
|
|
|
|
test('T003: core blocker keeps LOGIN_READY false and rollup failed_action_required', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get statefulset/db -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.canonical.tiers.core.ready, false);
|
|
assert.equal(status.canonical.tiers.login.ready, false);
|
|
assert.equal(status.canonical.rollup.state, 'failed_action_required');
|
|
});
|
|
|
|
test('T004: status dashboard model includes talos, cluster, flux, workloads, release, and config paths', async () => {
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(healthyResponses()),
|
|
});
|
|
|
|
assert.equal(status.connectivityMode, 'app-healthy');
|
|
assert.equal(status.topBlocker.layer, 'none');
|
|
assert.equal(status.host.status, 'healthy');
|
|
assert.equal(status.cluster.apiReachable, true);
|
|
assert.equal(status.flux.status, 'healthy');
|
|
assert.equal(status.workloads.components.length, 8);
|
|
assert.equal(status.release.selectedReleaseVersion, '1.0.0');
|
|
assert.equal(status.release.appUrl, 'https://psa.example.com');
|
|
assert.equal(status.configPaths.kubeconfig, '/tmp/kubeconfig');
|
|
});
|
|
|
|
test('T005: talos-only state reports Kubernetes availability as blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get --raw=/readyz'] = { ok: false, output: 'connection refused' };
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
assert.equal(status.connectivityMode, 'talos-only');
|
|
assert.equal(status.topBlocker.layer, 'Kubernetes availability');
|
|
});
|
|
|
|
test('T005: cluster-down with talos unreachable reports talos blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['talosctl --talosconfig /tmp/talosconfig -n 10.0.0.2 -e 10.0.0.2 health --wait-timeout 20s'] = {
|
|
ok: false,
|
|
output: 'dial tcp timeout',
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get --raw=/readyz'] = { ok: false, output: 'connection refused' };
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
assert.equal(status.connectivityMode, 'degraded');
|
|
assert.equal(status.topBlocker.layer, 'Talos host reachability');
|
|
});
|
|
|
|
test('T005: flux-degraded state reports Flux blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n flux-system get kustomizations.kustomize.toolkit.fluxcd.io -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [{ metadata: { name: 'alga-appliance' }, status: { conditions: readyCondition('False', 'reconcile failed') } }],
|
|
}),
|
|
};
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
assert.equal(status.topBlocker.layer, 'Flux source/reconcile failure');
|
|
});
|
|
|
|
test('T005: workload-unhealthy state reports workload blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/temporal-worker -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
assert.equal(status.workloads.status, 'unhealthy');
|
|
assert.equal(status.topBlocker.layer, 'workload readiness failure');
|
|
});
|
|
|
|
test('T004: DNS resolver failures are classified with explicit DNS remediation guidance', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'flux-system', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'lookup factory.talos.dev on 192.168.64.1:53: connection refused',
|
|
involvedObject: { kind: 'Pod', name: 'source-controller-abc' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Platform DNS resolution');
|
|
assert.match(status.topBlocker.reason, /DNS resolver failure detected/i);
|
|
assert.match(status.topBlocker.nextAction, /Configure explicit DNS servers/i);
|
|
});
|
|
|
|
test('T005: Postgres subPath failure is classified as a core storage blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get statefulset/db -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'failed to create subPath directory for volumeMount "db-data"',
|
|
involvedObject: { kind: 'Pod', name: 'db-0' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Core Postgres storage initialization');
|
|
assert.match(status.topBlocker.reason, /PVC\/subPath initialization failed/i);
|
|
assert.match(status.topBlocker.nextAction, /repair or recreate the Postgres PVC subPath/i);
|
|
});
|
|
|
|
test('T006: workflow-worker missing image tag is classified as background-only blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/workflow-worker -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'Failed to pull image "ghcr.io/nine-minds/workflow-worker:61e4a00e": not found',
|
|
involvedObject: { kind: 'Pod', name: 'workflow-worker-6c5f8f7d9b-abcde' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Image tag availability');
|
|
assert.equal(status.topBlocker.component, 'workflow-worker');
|
|
assert.equal(status.topBlocker.loginBlocking, false);
|
|
assert.match(status.topBlocker.reason, /workflow-worker:61e4a00e/i);
|
|
});
|
|
|
|
test('T007: alga-core missing image tag is classified as login-blocking blocker', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/alga-core-sebastian -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'ImagePullBackOff: Failed to pull image "ghcr.io/nine-minds/alga-psa-ee:deadbeef": not found',
|
|
involvedObject: { kind: 'Pod', name: 'alga-core-sebastian-5bd8b8d9f8-xxyyy' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Image tag availability');
|
|
assert.equal(status.topBlocker.component, 'alga-core');
|
|
assert.equal(status.topBlocker.loginBlocking, true);
|
|
assert.match(status.topBlocker.reason, /alga-psa-ee:deadbeef/i);
|
|
});
|
|
|
|
test('T008: image pull context canceled is classified as retryable interruption', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/email-service -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'Failed to pull image "ghcr.io/nine-minds/email-service:61e4a00e": context canceled',
|
|
involvedObject: { kind: 'Pod', name: 'email-service-7cc4f7f9d8-abcde' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Image pull interruption');
|
|
assert.equal(status.topBlocker.component, 'email-service');
|
|
assert.match(status.topBlocker.reason, /retryable/i);
|
|
assert.match(status.topBlocker.nextAction, /automatic retry/i);
|
|
});
|
|
|
|
test('image tag not found takes precedence over older interrupted pull events', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/workflow-worker -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'Failed to pull image "ghcr.io/nine-minds/workflow-worker:61e4a00e": context canceled',
|
|
involvedObject: { kind: 'Pod', name: 'workflow-worker-6c5f8f7d9b-abcde' },
|
|
},
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:01:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'Failed to pull image "ghcr.io/nine-minds/workflow-worker:61e4a00e": not found',
|
|
involvedObject: { kind: 'Pod', name: 'workflow-worker-6c5f8f7d9b-abcde' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Image tag availability');
|
|
assert.equal(status.topBlocker.component, 'workflow-worker');
|
|
assert.equal(status.topBlocker.loginBlocking, false);
|
|
});
|
|
|
|
test('recent events keep the latest entries instead of stale oldest entries', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: Array.from({ length: 25 }, (_, index) => ({
|
|
metadata: { namespace: 'msp', creationTimestamp: `2026-04-30T00:${String(index).padStart(2, '0')}:00Z` },
|
|
reason: 'Pulled',
|
|
type: 'Normal',
|
|
message: `event-${index}`,
|
|
involvedObject: { kind: 'Pod', name: `pod-${index}` },
|
|
})),
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.canonical.recentEvents.length, 20);
|
|
assert.equal(status.canonical.recentEvents[0].message, 'event-5');
|
|
assert.equal(status.canonical.recentEvents[19].message, 'event-24');
|
|
});
|
|
|
|
test('Talos client auth failure does not block LOGIN_READY when Kubernetes and core are healthy', async () => {
|
|
const responses = healthyResponses();
|
|
responses['talosctl --talosconfig /tmp/talosconfig -n 10.0.0.2 -e 10.0.0.2 health --wait-timeout 20s'] = {
|
|
ok: false,
|
|
output: 'x509: certificate signed by unknown authority',
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.host.status, 'unreachable');
|
|
assert.equal(status.canonical.tiers.platform.ready, true);
|
|
assert.equal(status.canonical.tiers.login.ready, true);
|
|
assert.equal(status.canonical.rollup.state, 'fully_healthy');
|
|
});
|
|
|
|
test('active core image pull reports installing state with size estimate instead of action required', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/alga-core-sebastian -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Pulling',
|
|
type: 'Normal',
|
|
message: 'Pulling image "ghcr.io/nine-minds/alga-psa-ee:94446747"',
|
|
involvedObject: { kind: 'Pod', name: 'alga-core-sebastian-5bd8b8d9f8-xxyyy' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.canonical.rollup.state, 'installing');
|
|
assert.match(status.canonical.rollup.message, /Pulling image ghcr\.io\/nine-minds\/alga-psa-ee:94446747/i);
|
|
assert.equal(status.canonical.activeOperations.length, 1);
|
|
assert.equal(status.canonical.activeOperations[0].component, 'alga-core');
|
|
assert.equal(status.canonical.activeOperations[0].estimatedSizeHuman, '~1.8 GB');
|
|
assert.equal(status.canonical.activeOperations[0].progressAvailable, false);
|
|
});
|
|
|
|
test('T009: helm timeout with db subPath failure reports db storage blocker as top cause', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n alga-system get helmreleases.helm.toolkit.fluxcd.io -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [{ metadata: { name: 'alga-core' }, status: { conditions: readyCondition('False', 'context deadline exceeded') } }],
|
|
}),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get statefulset/db -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'Failed',
|
|
type: 'Warning',
|
|
message: 'failed to create subPath directory for volumeMount "db-data"',
|
|
involvedObject: { kind: 'Pod', name: 'db-0' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.flux.helmStatus, 'unhealthy');
|
|
assert.equal(status.topBlocker.layer, 'Core Postgres storage initialization');
|
|
assert.match(status.topBlocker.reason, /subPath/i);
|
|
});
|
|
|
|
test('T010: bootstrap job completion plus seeded users query sets BOOTSTRAP_READY true', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n alga-system get helmreleases.helm.toolkit.fluxcd.io -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [{ metadata: { name: 'alga-core' }, status: { conditions: readyCondition('False', 'still reconciling') } }],
|
|
}),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get jobs.batch -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { name: 'alga-core-bootstrap' },
|
|
status: { conditions: [{ type: 'Complete', status: 'True' }], succeeded: 1 },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
responses[
|
|
"kubectl --kubeconfig /tmp/kubeconfig -n msp exec db-0 -- sh -c PGPASSWORD=$POSTGRES_PASSWORD psql -U postgres -d server -tAc 'select count(*) from users;'"
|
|
] = {
|
|
ok: true,
|
|
output: '7\n',
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.bootstrap.job.completed, true);
|
|
assert.equal(status.bootstrap.seed.usersCount, 7);
|
|
assert.equal(status.canonical.tiers.bootstrap.ready, true);
|
|
});
|
|
|
|
test('bootstrap job log error is promoted into canonical blocker and rollup', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get jobs.batch -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { name: 'alga-core-sebastian-bootstrap-r7' },
|
|
status: { conditions: [{ type: 'Failed', status: 'True' }], failed: 1 },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get pods -l job-name=alga-core-sebastian-bootstrap-r7 -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { name: 'alga-core-sebastian-bootstrap-r7-abc', creationTimestamp: '2026-04-30T00:01:00Z' },
|
|
spec: { containers: [{ name: 'bootstrap' }] },
|
|
status: { phase: 'Failed' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp logs alga-core-sebastian-bootstrap-r7-abc -c bootstrap --tail=160 --timestamps'] = {
|
|
ok: true,
|
|
output: [
|
|
'2026-04-30T00:01:00Z Running migrations',
|
|
'2026-04-30T00:01:01Z ERROR: Configured seed directory does not exist: /app/ee/server/seeds/onboarding',
|
|
].join('\n'),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Bootstrap job failure');
|
|
assert.match(status.topBlocker.reason, /Configured seed directory does not exist/);
|
|
assert.match(status.topBlocker.nextAction, /seed directory/);
|
|
assert.equal(status.canonical.topBlockers[0].layer, 'Bootstrap job failure');
|
|
assert.match(status.canonical.rollup.message, /Configured seed directory does not exist/);
|
|
assert.equal(status.canonical.bootstrap.logs.available, true);
|
|
assert.equal(status.canonical.bootstrap.logs.detectedErrors.length, 1);
|
|
});
|
|
|
|
test('T011: Temporal schema compatibility errors are classified with autosetup guidance', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/temporal -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'BackOff',
|
|
type: 'Warning',
|
|
message: 'sql schema version compatibility check failed',
|
|
involvedObject: { kind: 'Pod', name: 'temporal-abcde' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Temporal schema initialization');
|
|
assert.match(status.topBlocker.reason, /schema not initialized/i);
|
|
assert.match(status.topBlocker.nextAction, /autosetup/i);
|
|
});
|
|
|
|
test('T012: Temporal UI service-link collision errors are classified with disable-service-links guidance', async () => {
|
|
const responses = healthyResponses();
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig -n msp get deployment/temporal -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({ spec: { replicas: 1 }, status: { readyReplicas: 0 } }),
|
|
};
|
|
responses['kubectl --kubeconfig /tmp/kubeconfig get events --sort-by=.metadata.creationTimestamp -A -o json'] = {
|
|
ok: true,
|
|
output: JSON.stringify({
|
|
items: [
|
|
{
|
|
metadata: { namespace: 'msp', creationTimestamp: '2026-04-30T00:00:00Z' },
|
|
reason: 'BackOff',
|
|
type: 'Warning',
|
|
message: 'cannot unmarshal !!str tcp://10.96.0.1:443 into int',
|
|
involvedObject: { kind: 'Pod', name: 'temporal-ui-abcde' },
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
|
|
const status = await collectStatus(buildEnv(), {
|
|
runner: new MockCaptureRunner(responses),
|
|
});
|
|
|
|
assert.equal(status.topBlocker.layer, 'Kubernetes service-link environment collision');
|
|
assert.match(status.topBlocker.reason, /service-link environment collision/i);
|
|
assert.match(status.topBlocker.nextAction, /Disable service links/i);
|
|
});
|