e2646763c0
Adds read-only D601 recovery diagnostics, fixture coverage, CLI wiring, and recovery hotfix runbook updates. Validated with recovery contract, check --files, scripts tsc, artifact matrix direct contract, and read-only live diagnostic.
158 lines
8.4 KiB
TypeScript
158 lines
8.4 KiB
TypeScript
import { readConfig } from "./src/config";
|
|
import { extractHostPathEntries, parseContainerCreatingReport, parseProcMounts, runD601RecoveryGuardrails, type RecoveryGuardrailsFixture } from "./src/recovery-guardrails";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
const malformedProcMounts = [
|
|
"proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0",
|
|
"drvfs /Docker/host 9p rw,dirsync,aname=drvfs;path=C:\\Program Files\\Docker\\Docker Desktop\\host 0 0",
|
|
].join("\n");
|
|
|
|
const manifest = `
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: code-queue-scheduler-dev
|
|
namespace: unidesk-dev
|
|
spec:
|
|
template:
|
|
spec:
|
|
volumes:
|
|
- name: repo
|
|
hostPath:
|
|
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
|
|
type: Directory
|
|
- name: state
|
|
hostPath:
|
|
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue
|
|
type: DirectoryOrCreate
|
|
---
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: mdtodo-dev
|
|
namespace: unidesk-dev
|
|
spec:
|
|
template:
|
|
spec:
|
|
volumes:
|
|
- name: workspace
|
|
hostPath:
|
|
path: /home/ubuntu/unidesk-dev-mdtodo-workspace
|
|
type: Directory
|
|
- name: logs
|
|
hostPath:
|
|
path: /home/ubuntu/cq-deploy/.state/mdtodo-dev/logs
|
|
type: DirectoryOrCreate
|
|
`;
|
|
|
|
const kubectlPods = {
|
|
items: [
|
|
{
|
|
metadata: { namespace: "unidesk-dev", name: "code-queue-scheduler-dev-abc" },
|
|
status: {
|
|
containerStatuses: [{
|
|
name: "code-queue",
|
|
state: {
|
|
waiting: {
|
|
reason: "ContainerCreating",
|
|
message: "MountVolume.SetUp failed for volume \"repo\": hostPath type check failed: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue is not a directory",
|
|
},
|
|
},
|
|
}],
|
|
},
|
|
},
|
|
{
|
|
metadata: { namespace: "unidesk-dev", name: "mdtodo-dev-def" },
|
|
status: {
|
|
containerStatuses: [{
|
|
name: "mdtodo",
|
|
state: { waiting: { reason: "ContainerCreating", message: "" } },
|
|
}],
|
|
},
|
|
},
|
|
],
|
|
};
|
|
|
|
const staleCriPods = {
|
|
items: [
|
|
{
|
|
id: "sandbox-stale",
|
|
metadata: { name: "code-queue-scheduler-dev-abc", namespace: "unidesk-dev" },
|
|
state: "SANDBOX_NOTREADY",
|
|
createdAt: "2026-05-23T09:00:00.000Z",
|
|
},
|
|
],
|
|
};
|
|
|
|
export function runD601RecoveryGuardrailsContract(): JsonRecord {
|
|
const procMounts = parseProcMounts(malformedProcMounts, "fixture:/proc/mounts");
|
|
assertCondition(procMounts.ok === false, "malformed /proc/mounts fixture should fail", procMounts);
|
|
assertCondition(procMounts.malformedLines.length === 1, "one malformed mount line expected", procMounts);
|
|
assertCondition(procMounts.malformedLines[0]?.dockerDesktopHost9p === true, "malformed line should be classified as Docker Desktop /Docker/host 9p", procMounts);
|
|
|
|
const entries = extractHostPathEntries("fixture.k8s.yaml", manifest);
|
|
assertCondition(entries.some((entry) => entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue"), "hostPath parser should extract repo path", entries);
|
|
assertCondition(entries.some((entry) => entry.hostPath === "/home/ubuntu/unidesk-dev-mdtodo-workspace"), "hostPath parser should extract MDTODO workspace path", entries);
|
|
|
|
const fixture: RecoveryGuardrailsFixture = {
|
|
observedAt: "2026-05-23T10:00:00.000Z",
|
|
procMountsText: malformedProcMounts,
|
|
criPodsJsonText: JSON.stringify(staleCriPods),
|
|
kubectlPodsJsonText: JSON.stringify(kubectlPods),
|
|
deployWorktreePath: "/home/ubuntu/unidesk-code-queue-deploy",
|
|
compatibilitySymlinkPath: "/home/ubuntu/cq-deploy",
|
|
manifestPaths: ["fixture.k8s.yaml"],
|
|
manifestTexts: { "fixture.k8s.yaml": manifest },
|
|
pathStates: {
|
|
"/home/ubuntu/unidesk-code-queue-deploy": { kind: "missing" },
|
|
"/home/ubuntu/cq-deploy": { kind: "symlink", target: "/home/ubuntu/unidesk-code-queue-deploy", targetExists: false },
|
|
"/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue": { kind: "missing" },
|
|
"/home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue": { kind: "missing" },
|
|
"/home/ubuntu/unidesk-dev-mdtodo-workspace": { kind: "missing" },
|
|
"/home/ubuntu/cq-deploy/.state/mdtodo-dev/logs": { kind: "missing" },
|
|
},
|
|
};
|
|
const result = runD601RecoveryGuardrails(readConfig(), fixture);
|
|
assertCondition(result.scope.liveMutationAllowed === false && result.mutation === false, "guardrail result must be read-only", result.scope);
|
|
assertCondition(result.ok === false, "fixture should produce red guardrails", result.redlineSummary);
|
|
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("proc-mounts-malformed-kubelet-risk"), "malformed mount risk should require manual host hotfix", result.redlineSummary);
|
|
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("code-queue-deploy-worktree-not-ready"), "missing worktree/symlink target should require manual host hotfix", result.redlineSummary);
|
|
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("required-hostpaths-not-ready"), "missing hostPath should require manual host hotfix", result.redlineSummary);
|
|
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("opaque-containercreating-hostpath-risk"), "ContainerCreating hostPath risk should require manual host hotfix", result.redlineSummary);
|
|
assertCondition(result.checks.codeQueueDeployWorktree.canonicalPath.exists === false, "canonical deploy worktree should report missing", result.checks.codeQueueDeployWorktree);
|
|
assertCondition(result.checks.codeQueueDeployWorktree.compatibilitySymlink.isSymlink === true, "compat symlink should be visible", result.checks.codeQueueDeployWorktree);
|
|
assertCondition(result.checks.codeQueueDeployWorktree.compatibilitySymlink.targetExists === false, "compat symlink target should report missing", result.checks.codeQueueDeployWorktree);
|
|
assertCondition(result.checks.hostPaths.missing.some((problem) => problem.entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue"), "missing Directory hostPath should be red", result.checks.hostPaths.missing);
|
|
assertCondition(result.checks.hostPaths.directoryOrCreateMissing.some((problem) => problem.entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue"), "DirectoryOrCreate missing should be surfaced separately", result.checks.hostPaths.directoryOrCreateMissing);
|
|
assertCondition(result.checks.mdtodoAdjacentHostPaths.opaqueRisk === true, "MDTODO adjacent hostPath readiness should be opaque when workspace/logs missing", result.checks.mdtodoAdjacentHostPaths);
|
|
assertCondition(result.checks.criSandboxes.staleNotReadyCount === 1, "stale CRI sandbox should be counted", result.checks.criSandboxes);
|
|
assertCondition(result.checks.containerCreating.hostPathContainerCreating.length === 1, "explicit hostPath ContainerCreating should be classified", result.checks.containerCreating);
|
|
assertCondition(result.checks.containerCreating.opaqueContainerCreating.length === 1, "empty-message ContainerCreating should be classified as opaque when hostPaths are missing", result.checks.containerCreating);
|
|
assertCondition(result.redlineSummary.forbiddenAutomaticActions.includes("crictl rmp"), "CRI deletion should be explicitly forbidden", result.redlineSummary);
|
|
assertCondition(result.redlineSummary.forbiddenAutomaticActions.includes("systemctl restart k3s"), "k3s restart should be explicitly forbidden", result.redlineSummary);
|
|
|
|
const containerCreatingOnly = parseContainerCreatingReport(JSON.stringify(kubectlPods), result.checks.hostPaths, "fixture", null);
|
|
assertCondition(containerCreatingOnly.ok === false, "ContainerCreating parser should fail when opaque/hostPath findings exist", containerCreatingOnly);
|
|
|
|
return {
|
|
ok: true,
|
|
checks: [
|
|
"malformed /proc/mounts Docker Desktop /Docker/host 9p line detected",
|
|
"missing Code Queue worktree symlink and target reported",
|
|
"missing hostPath and DirectoryOrCreate readiness reported",
|
|
"opaque and explicit hostPath ContainerCreating classified",
|
|
"stale CRI sandbox count reported without cleanup",
|
|
"destructive prune/reset/restart/delete actions forbidden",
|
|
],
|
|
};
|
|
}
|
|
|
|
if (import.meta.main) {
|
|
process.stdout.write(`${JSON.stringify(runD601RecoveryGuardrailsContract(), null, 2)}\n`);
|
|
}
|