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
704 lines
24 KiB
JavaScript
704 lines
24 KiB
JavaScript
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { collectStatusSnapshot, collectStatusSnapshotAsync } from '../status-engine.mjs';
|
|
|
|
const kubectlUnavailableRunner = (command) => Promise.resolve({
|
|
ok: false,
|
|
status: 127,
|
|
command,
|
|
stdout: '',
|
|
stderr: 'sh: 1: kubectl: not found'
|
|
});
|
|
|
|
function writeStateFile(state) {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-net-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
fs.writeFileSync(stateFile, JSON.stringify(state));
|
|
return stateFile;
|
|
}
|
|
|
|
test('collectStatusSnapshot reads local state and kubeconfig-driven kubectl output', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
const setupInputsFile = path.join(tmp, 'setup-inputs.json');
|
|
const releaseSelectionFile = path.join(tmp, 'release-selection.json');
|
|
fs.writeFileSync(stateFile, JSON.stringify({ phase: 'flux', status: 'flux-source-complete' }));
|
|
fs.writeFileSync(setupInputsFile, JSON.stringify({ channel: 'stable', appHostname: 'http://192.0.2.10:3000', releaseRef: '' }));
|
|
fs.writeFileSync(releaseSelectionFile, JSON.stringify({ selectedChannel: 'stable', selectedReleaseVersion: '1.0.0', registryHost: 'ghcr.io', repository: 'nine-minds/alga-appliance-release', manifestDigest: 'sha256:release' }));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
default pod-a Running
|
|
kube-system pod-b Running
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
setupInputsFile,
|
|
releaseSelectionFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.currentPhase, 'flux');
|
|
assert.equal(snapshot.status, 'flux-source-complete');
|
|
assert.equal(snapshot.setupInputs.channel, 'stable');
|
|
assert.equal(snapshot.urls.loginUrl, 'http://192.0.2.10:3000');
|
|
assert.equal(snapshot.releaseSelection.manifestDigest, 'sha256:release');
|
|
assert.equal(snapshot.kubernetes.nodes.length, 1);
|
|
assert.equal(snapshot.kubernetes.nodes[0].ready, true);
|
|
assert.equal(snapshot.kubernetes.podCount, 2);
|
|
assert.equal(snapshot.kubernetes.warnings.length, 0);
|
|
assert.equal(snapshot.tiers.platformReady, true);
|
|
assert.equal(snapshot.tiers.coreReady, false);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('collectStatusSnapshot includes UI contract fields for live status page', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-ui-contract-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
fs.writeFileSync(stateFile, JSON.stringify({ phase: 'registry-release-source', status: 'release-config-complete', lastAction: 'Release selection persisted.' }));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
msp alga-core-abc Running
|
|
msp temporal-worker-xyz CrashLoopBackOff
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core-bootstrap 1/1 1 1m
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core 1h True Helm install succeeded
|
|
temporal-worker 1h False Helm install failed
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get events -A -o json"* ]]; then
|
|
echo '{"items":[]}'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.readinessTiers.platformReady.ready, true);
|
|
assert.equal(snapshot.readinessTiers.backgroundReady.ready, false);
|
|
assert.equal(snapshot.topBlockers.length >= 1, true);
|
|
assert.equal(typeof snapshot.rollup.state, 'string');
|
|
assert.equal(Array.isArray(snapshot.recentEvents), true);
|
|
assert.equal(Array.isArray(snapshot.activeOperations), true);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('loginReady remains true when background service has issues', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-bg-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({ phase: 'app-readiness', status: 'release-config-complete' }));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
msp alga-core-abc Running
|
|
alga-system temporal-worker-xyz CrashLoopBackOff
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core-bootstrap 1/1 1 1m
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core 1h True Release reconciliation succeeded
|
|
TXT
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.tiers.loginReady, true);
|
|
assert.equal(snapshot.tiers.backgroundReady, false);
|
|
assert.equal(snapshot.tiers.backgroundIssues.length, 1);
|
|
assert.equal(snapshot.failures.some((failure) => failure.category === 'background-services'), true);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('non-ready HelmReleases block fullyHealthy even when pods are running', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-hr-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({ phase: 'app-readiness', status: 'release-config-complete' }));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
msp alga-core-abc Running
|
|
msp workflow-worker-xyz Running
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core-bootstrap 1/1 1 1m
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core 1h True Helm install succeeded
|
|
workflow-worker 1h False Helm install failed for release msp/workflow-worker
|
|
TXT
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.tiers.loginReady, true);
|
|
assert.equal(snapshot.tiers.backgroundReady, false);
|
|
assert.equal(snapshot.tiers.fullyHealthy, false);
|
|
assert.equal(snapshot.failures.some((failure) => failure.category === 'flux'), true);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('missing expected HelmReleases block fullyHealthy while staged kustomizations are still catching up', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-hr-missing-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({ phase: 'app-readiness', status: 'release-config-complete' }));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
msp alga-core-abc Running
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core-bootstrap 1/1 1 1m
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core 1h True Helm install succeeded
|
|
pgbouncer 1h True Helm install succeeded
|
|
TXT
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.tiers.loginReady, true);
|
|
assert.equal(snapshot.tiers.fullyHealthy, false);
|
|
assert.equal(snapshot.failures.some((failure) => failure.suspectedCause.includes('temporal missing')), true);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('early setup treats missing kubectl as expected install progress, not a blocker', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-early-kubectl-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase: 'setup',
|
|
status: 'setup-accepted',
|
|
lastAction: 'Setup accepted; background workflow is starting'
|
|
}));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
echo 'sh: 1: kubectl: not found' >&2
|
|
exit 127
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.rollup.state, 'installing');
|
|
assert.equal(snapshot.rollup.message, 'Starting the appliance installation.');
|
|
assert.equal(snapshot.tiers.platformReady, false);
|
|
assert.equal(snapshot.readinessTiers.platformReady.status, 'waiting_for_kubernetes');
|
|
assert.equal(snapshot.failures.length, 0);
|
|
assert.equal(snapshot.topBlockers.length, 0);
|
|
assert.equal(snapshot.kubernetes.warnings.length, 0);
|
|
assert.equal(snapshot.kubernetes.suppressedWarnings.length, 1);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('app-readiness treats HelmRelease dependency convergence as progress, not a blocker', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-readiness-helm-converge-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase: 'app-readiness',
|
|
status: 'release-config-complete',
|
|
lastAction: 'Checking application readiness.'
|
|
}));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
msp alga-core-abc Running
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
cat <<'TXT'
|
|
alga-core 4m38s Unknown Running 'install' action with timeout of 30m0s
|
|
email-service 4m38s False dependency 'alga-system/alga-core' is not ready
|
|
pgbouncer 4m37s False dependency 'alga-system/alga-core' is not ready
|
|
temporal 4m37s False dependency 'alga-system/alga-core' is not ready
|
|
temporal-worker 4m36s False dependency 'alga-system/alga-core' is not ready
|
|
workflow-worker 4m35s False dependency 'alga-system/alga-core' is not ready
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get events -A -o json"* ]]; then
|
|
echo '{"items":[]}'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.rollup.state, 'installing');
|
|
assert.equal(snapshot.tiers.platformReady, true);
|
|
assert.equal(snapshot.tiers.loginReady, false);
|
|
assert.equal(snapshot.tiers.backgroundReady, false);
|
|
assert.equal(snapshot.tiers.fullyHealthy, false);
|
|
assert.equal(snapshot.kubernetes.helmReleaseCount, 6);
|
|
assert.equal(snapshot.failures.length, 0);
|
|
assert.equal(snapshot.topBlockers.length, 0);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('app-readiness treats missing HelmRelease CRD as transient progress, not a blocker', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-readiness-helm-crd-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase: 'app-readiness',
|
|
status: 'release-config-complete',
|
|
lastAction: 'Checking application readiness.'
|
|
}));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
if [[ "$*" == *"get nodes -o json"* ]]; then
|
|
cat <<'JSON'
|
|
{"items":[{"metadata":{"name":"node-1"},"status":{"conditions":[{"type":"Ready","status":"True"}]}}]}
|
|
JSON
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"get pods -A --no-headers"* ]]; then
|
|
cat <<'TXT'
|
|
kube-system flux-controller-abc Running
|
|
TXT
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n msp get jobs --no-headers"* ]]; then
|
|
exit 0
|
|
fi
|
|
if [[ "$*" == *"-n alga-system get helmreleases"* ]]; then
|
|
printf '%s\n' "error: the server doesn't have a resource type \"helmreleases\"" >&2
|
|
exit 1
|
|
fi
|
|
if [[ "$*" == *"get events -A -o json"* ]]; then
|
|
echo '{"items":[]}'
|
|
exit 0
|
|
fi
|
|
exit 1
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.rollup.state, 'installing');
|
|
assert.equal(snapshot.rollup.message, 'Checking application readiness.');
|
|
assert.equal(snapshot.tiers.platformReady, true);
|
|
assert.equal(snapshot.tiers.loginReady, false);
|
|
assert.equal(snapshot.tiers.fullyHealthy, false);
|
|
assert.equal(snapshot.failures.length, 0);
|
|
assert.equal(snapshot.topBlockers.length, 0);
|
|
assert.equal(snapshot.kubernetes.warnings.length, 0);
|
|
assert.equal(snapshot.kubernetes.suppressedWarnings.length, 1);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('app-readiness still reports kubectl query failures as blockers', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-readiness-kubectl-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase: 'app-readiness',
|
|
status: 'release-config-complete',
|
|
lastAction: 'Checking application readiness.'
|
|
}));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, `#!/usr/bin/env bash
|
|
echo 'sh: 1: kubectl: not found' >&2
|
|
exit 127
|
|
`);
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.rollup.state, 'blocked');
|
|
assert.equal(snapshot.failures.length, 1);
|
|
assert.equal(snapshot.failures[0].category, 'app-readiness');
|
|
assert.equal(snapshot.topBlockers.length, 1);
|
|
assert.equal(snapshot.kubernetes.warnings.length, 1);
|
|
assert.equal(snapshot.kubernetes.suppressedWarnings.length, 0);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('collectStatusSnapshot classifies persisted k3s failures', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-k3s-failure-'));
|
|
const stateFile = path.join(tmp, 'install-state.json');
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase: 'k3s',
|
|
status: 'k3s-install-blocked',
|
|
lastAction: 'k3s installation command failed.',
|
|
failure: {
|
|
phase: 'k3s',
|
|
step: 'install-k3s-server',
|
|
message: 'k3s installation command failed.',
|
|
suspectedCause: 'k3s install failed',
|
|
suggestedNextStep: 'inspect logs',
|
|
retrySafe: true
|
|
}
|
|
}));
|
|
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, '#!/usr/bin/env bash\nexit 1\n');
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
|
|
assert.equal(snapshot.failures.length >= 1, true);
|
|
assert.equal(snapshot.failures[0].category, 'k3s');
|
|
assert.equal(snapshot.failures[0].retrySafe, true);
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|
|
|
|
test('live network probe clears a stale recorded network failure and unpoisons k8s suppression', async () => {
|
|
const stateFile = writeStateFile({
|
|
phase: 'network',
|
|
status: 'preflight-blocked',
|
|
lastAction: 'Network failure while fetching GitHub channel metadata.',
|
|
failure: {
|
|
phase: 'network',
|
|
step: 'fetch-channel-metadata',
|
|
message: 'Network failure while fetching GitHub channel metadata.',
|
|
suspectedCause: 'Network failure while fetching GitHub channel metadata.',
|
|
suggestedNextStep: 'Check outbound HTTPS and proxy settings. Invalid IP address: undefined',
|
|
retrySafe: true
|
|
}
|
|
});
|
|
|
|
const snapshot = await collectStatusSnapshotAsync({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml',
|
|
runCommand: kubectlUnavailableRunner,
|
|
networkProbe: { ok: true, checkedAt: '2026-05-28T23:59:00.000Z', failure: null }
|
|
});
|
|
|
|
// The stale "Invalid IP address: undefined" text is gone; one accurate retry blocker remains.
|
|
assert.equal(snapshot.network.ok, true);
|
|
assert.equal(snapshot.lastRecordedError.resolvedByLiveCheck, true);
|
|
assert.equal(snapshot.failures.length, 1);
|
|
assert.equal(snapshot.failures[0].category, 'network');
|
|
assert.equal(snapshot.failures[0].resolved, true);
|
|
assert.equal(snapshot.failures.some((f) => String(f.suggestedNextStep).includes('Invalid IP address')), false);
|
|
// The cleared network failure no longer poisons early-kubernetes suppression.
|
|
assert.equal(snapshot.kubernetes.warnings.length, 0);
|
|
assert.equal(snapshot.kubernetes.suppressedWarnings.length >= 1, true);
|
|
assert.equal(snapshot.rollup.state, 'blocked');
|
|
});
|
|
|
|
test('live network probe failure surfaces a fresh blocker instead of the recorded one', async () => {
|
|
const stateFile = writeStateFile({
|
|
phase: 'network',
|
|
status: 'preflight-blocked',
|
|
failure: { phase: 'network', step: 'fetch-channel-metadata', message: 'old recorded text', suspectedCause: 'old recorded text', suggestedNextStep: 'old', retrySafe: true }
|
|
});
|
|
|
|
const snapshot = await collectStatusSnapshotAsync({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml',
|
|
runCommand: kubectlUnavailableRunner,
|
|
networkProbe: {
|
|
ok: false,
|
|
checkedAt: '2026-05-28T23:59:30.000Z',
|
|
failure: { phase: 'network', step: 'reach-ghcr', message: 'Network failure while contacting ghcr.io.', suspectedCause: 'Network failure while contacting ghcr.io.', suggestedNextStep: 'Check outbound HTTPS and proxy settings for GHCR.', retrySafe: true }
|
|
}
|
|
});
|
|
|
|
assert.equal(snapshot.network.ok, false);
|
|
assert.equal(snapshot.failures.length, 1);
|
|
assert.equal(snapshot.failures[0].category, 'network');
|
|
assert.equal(snapshot.failures[0].checkedAt, '2026-05-28T23:59:30.000Z');
|
|
assert.equal(snapshot.failures[0].suspectedCause, 'Network failure while contacting ghcr.io.');
|
|
assert.equal(snapshot.rollup.state, 'blocked');
|
|
});
|
|
|
|
test('live network probe does not alter a recorded non-network (k3s) failure', async () => {
|
|
const stateFile = writeStateFile({
|
|
phase: 'k3s',
|
|
status: 'k3s-install-blocked',
|
|
failure: { phase: 'k3s', step: 'install-k3s-server', message: 'k3s installation command failed.', suspectedCause: 'k3s install failed', suggestedNextStep: 'inspect logs', retrySafe: true }
|
|
});
|
|
|
|
const snapshot = await collectStatusSnapshotAsync({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml',
|
|
runCommand: kubectlUnavailableRunner,
|
|
networkProbe: { ok: true, checkedAt: '2026-05-28T23:59:45.000Z', failure: null }
|
|
});
|
|
|
|
assert.equal(snapshot.network.ok, true);
|
|
assert.equal(snapshot.lastRecordedError, null);
|
|
assert.equal(snapshot.failures.some((f) => f.category === 'k3s'), true);
|
|
});
|
|
|
|
test('collectStatusSnapshot maps failure phases to expected categories', () => {
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'alga-status-phase-map-'));
|
|
const fakeBin = path.join(tmp, 'bin');
|
|
fs.mkdirSync(fakeBin, { recursive: true });
|
|
const kubectlPath = path.join(fakeBin, 'kubectl');
|
|
fs.writeFileSync(kubectlPath, '#!/usr/bin/env bash\nexit 1\n');
|
|
fs.chmodSync(kubectlPath, 0o755);
|
|
|
|
const originalPath = process.env.PATH;
|
|
process.env.PATH = `${fakeBin}:${originalPath}`;
|
|
try {
|
|
const cases = [
|
|
['flux', 'flux-install-blocked', 'flux'],
|
|
['storage', 'storage-config-blocked', 'storage'],
|
|
['app-bootstrap', 'bootstrap-blocked', 'app-bootstrap'],
|
|
['app-readiness', 'readiness-blocked', 'app-readiness']
|
|
];
|
|
|
|
for (const [phase, status, expectedCategory] of cases) {
|
|
const stateFile = path.join(tmp, `${phase}.json`);
|
|
fs.writeFileSync(stateFile, JSON.stringify({
|
|
phase,
|
|
status,
|
|
lastAction: `${phase} failed`,
|
|
failure: {
|
|
phase,
|
|
step: `${phase}-step`,
|
|
message: `${phase} failed`,
|
|
suspectedCause: `${phase} cause`,
|
|
suggestedNextStep: `${phase} next`,
|
|
retrySafe: true
|
|
}
|
|
}));
|
|
|
|
const snapshot = collectStatusSnapshot({
|
|
stateFile,
|
|
kubeconfigPath: '/tmp/k3s.yaml',
|
|
kubectlPrefix: 'kubectl --kubeconfig /tmp/k3s.yaml'
|
|
});
|
|
assert.equal(snapshot.failures[0].category, expectedCategory);
|
|
}
|
|
} finally {
|
|
process.env.PATH = originalPath;
|
|
}
|
|
});
|