Files
pikasTech-unidesk/scripts/d601-recovery-guardrails-contract-test.ts
T
Lyon e2646763c0 feat: add d601 recovery guardrails
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.
2026-05-23 21:18:44 +08:00

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`);
}