737 lines
49 KiB
TypeScript
737 lines
49 KiB
TypeScript
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { spawnSync } from "node:child_process";
|
|
import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability";
|
|
import { buildDevContainerPlan, configureProviderRuntime, providerRuntimeForTest } from "../src/components/microservices/code-queue/src/provider-runtime";
|
|
import { codexPrPreflightQueryForTest } from "./src/code-queue";
|
|
import { summarizeMicroserviceObservation } from "./src/microservices";
|
|
|
|
type JsonRecord = Record<string, unknown>;
|
|
|
|
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
|
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
|
}
|
|
|
|
function asRecord(value: unknown, label: string): JsonRecord {
|
|
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
|
|
return value as JsonRecord;
|
|
}
|
|
|
|
function countOccurrences(haystack: string, needle: string): number {
|
|
return haystack.split(needle).length - 1;
|
|
}
|
|
|
|
function gitShowText(commit: string, path: string): string {
|
|
const result = spawnSync("git", ["show", `${commit}:${path}`], {
|
|
cwd: process.cwd(),
|
|
encoding: "utf8",
|
|
maxBuffer: 2 * 1024 * 1024,
|
|
});
|
|
assertCondition(result.status === 0, `git show should read ${path} at ${commit}`, {
|
|
status: result.status,
|
|
stderr: result.stderr.slice(-1000),
|
|
});
|
|
return result.stdout;
|
|
}
|
|
|
|
function createSkillSet(root: string, skills: string[]): void {
|
|
for (const skill of skills) {
|
|
const dir = join(root, skill);
|
|
mkdirSync(dir, { recursive: true });
|
|
writeFileSync(join(dir, "SKILL.md"), `---\nname: ${skill}\n---\n# ${skill}\n`);
|
|
}
|
|
}
|
|
|
|
const productionManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/code-queue.k8s.yaml", "utf8");
|
|
const devManifest = readFileSync("src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml", "utf8");
|
|
const deployJson = JSON.parse(readFileSync("deploy.json", "utf8")) as {
|
|
environments?: {
|
|
dev?: {
|
|
services?: Array<Record<string, unknown>>;
|
|
};
|
|
};
|
|
};
|
|
const runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8");
|
|
const indexSource = readFileSync("src/components/microservices/code-queue/src/index.ts", "utf8");
|
|
const skillModule = readFileSync("src/components/microservices/code-queue/src/skill-availability.ts", "utf8");
|
|
const codeQueueCli = readFileSync("scripts/src/code-queue.ts", "utf8");
|
|
const microserviceCli = readFileSync("scripts/src/microservices.ts", "utf8");
|
|
const helpSource = readFileSync("scripts/src/help.ts", "utf8");
|
|
const promptSource = readFileSync("src/components/microservices/code-queue/src/prompts.ts", "utf8");
|
|
const docsReference = readFileSync("docs/reference/code-queue-supervision.md", "utf8");
|
|
const forbiddenPathLiteral = [".ag", "nets/skills"].join("");
|
|
const forbiddenTargetPath = [`/root/${[".ag", "nets"].join("")}`, "skills"].join("/");
|
|
const forbiddenSourcePath = [`/home/ubuntu/${[".ag", "nets"].join("")}`, "skills"].join("/");
|
|
|
|
assertCondition(!productionManifest.includes(forbiddenPathLiteral), "production manifest must not propagate misspelled skills path");
|
|
assertCondition(!devManifest.includes(forbiddenPathLiteral), "dev manifest must not propagate misspelled skills path");
|
|
assertCondition(!promptSource.includes(forbiddenPathLiteral), "runner prompt must not mention misspelled skills path");
|
|
assertCondition(!skillModule.includes(forbiddenPathLiteral), "skill availability implementation must not propagate misspelled skills path literal");
|
|
assertCondition(!docsReference.includes(forbiddenPathLiteral), "reference docs must not propagate misspelled skills path literal");
|
|
assertCondition(countOccurrences(productionManifest, "name: UNIDESK_SKILLS_PATH") === 3, "production read/write/scheduler must set UNIDESK_SKILLS_PATH", {
|
|
count: countOccurrences(productionManifest, "name: UNIDESK_SKILLS_PATH"),
|
|
});
|
|
assertCondition(countOccurrences(productionManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH") === 3, "production read/write/scheduler must expose the approved runner skills source path", {
|
|
count: countOccurrences(productionManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH"),
|
|
});
|
|
assertCondition(countOccurrences(productionManifest, "mountPath: /root/.agents/skills") === 3, "production read/write/scheduler must mount skills target", {
|
|
count: countOccurrences(productionManifest, "mountPath: /root/.agents/skills"),
|
|
});
|
|
assertCondition(countOccurrences(productionManifest, "path: /home/ubuntu/.agents/skills") === 3, "production read/write/scheduler must use hostPath source of truth", {
|
|
count: countOccurrences(productionManifest, "path: /home/ubuntu/.agents/skills"),
|
|
});
|
|
assertCondition(countOccurrences(productionManifest, "name: skills-dir") >= 6, "production manifest must define skills-dir mounts and volumes", {
|
|
count: countOccurrences(productionManifest, "name: skills-dir"),
|
|
});
|
|
assertCondition(devManifest.includes("path: /home/ubuntu/.agents/skills"), "dev manifest should keep the same hostPath source of truth");
|
|
assertCondition(countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH") === 3, "dev read/write/scheduler must expose the approved runner skills source path", {
|
|
count: countOccurrences(devManifest, "name: CODE_QUEUE_RUNNER_SKILLS_SOURCE_PATH"),
|
|
});
|
|
|
|
const devCodeQueueDeploy = (deployJson.environments?.dev?.services ?? []).find((service) => service.id === "code-queue");
|
|
assertCondition(devCodeQueueDeploy !== undefined, "deploy.json dev environment must include code-queue");
|
|
const devCodeQueueCommit = String(devCodeQueueDeploy?.commitId ?? "");
|
|
assertCondition(/^[0-9a-f]{40}$/u.test(devCodeQueueCommit), "deploy.json dev code-queue commit must be a full SHA", devCodeQueueDeploy);
|
|
assertCondition(devCodeQueueCommit !== "0cf73d817f14032ad6038fd47ec402c87bf059bb", "deploy.json dev code-queue must not pin the pre-skills-mount source commit", devCodeQueueDeploy);
|
|
assertCondition(asRecord(devCodeQueueDeploy?.artifact, "dev code-queue artifact").repository === "unidesk/code-queue", "deploy.json dev code-queue must own artifact repository", devCodeQueueDeploy);
|
|
const devCodeQueueConsumer = asRecord(devCodeQueueDeploy?.consumer, "dev code-queue consumer");
|
|
assertCondition(devCodeQueueConsumer.kind === "d601-k3s-managed", "deploy.json dev code-queue must use the D601 k3s artifact consumer", devCodeQueueConsumer);
|
|
const devCodeQueueTarget = asRecord(devCodeQueueConsumer.target, "dev code-queue consumer target");
|
|
assertCondition(devCodeQueueTarget.manifestRepoPath === "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml", "deploy.json dev code-queue must point at the dev k3s manifest", devCodeQueueTarget);
|
|
|
|
const pinnedDevManifest = gitShowText(devCodeQueueCommit, "src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml");
|
|
const pinnedRuntimePreflight = gitShowText(devCodeQueueCommit, "src/components/microservices/code-queue/src/runtime-preflight.ts");
|
|
const pinnedIndexSource = gitShowText(devCodeQueueCommit, "src/components/microservices/code-queue/src/index.ts");
|
|
const pinnedProviderRuntime = gitShowText(devCodeQueueCommit, "src/components/microservices/code-queue/src/provider-runtime.ts");
|
|
assertCondition(countOccurrences(pinnedDevManifest, "path: /home/ubuntu/.agents/skills") === 3, "deploy.json dev code-queue commit must include source skills hostPath for scheduler/read/write", {
|
|
commit: devCodeQueueCommit,
|
|
});
|
|
assertCondition(countOccurrences(pinnedDevManifest, "mountPath: /root/.agents/skills") === 3, "deploy.json dev code-queue commit must mount skills target for scheduler/read/write", {
|
|
commit: devCodeQueueCommit,
|
|
});
|
|
assertCondition(!pinnedDevManifest.includes(forbiddenPathLiteral), "deploy.json dev code-queue commit must not include the misspelled skills path");
|
|
assertCondition(pinnedRuntimePreflight.includes("skills.contractOk && ports.codex.ok"), "deploy.json dev code-queue commit runtime-preflight must require target projection contract");
|
|
assertCondition(pinnedIndexSource.includes("skills.contractOk === true"), "deploy.json dev code-queue commit dev-ready must require target projection contract");
|
|
assertCondition(pinnedIndexSource.includes("return config.skillsPath"), "deploy.json dev code-queue commit must keep runner UNIDESK_SKILLS_PATH on the configured target");
|
|
assertCondition(pinnedProviderRuntime.includes("SKILLS_MOUNT_ARGS=(-v \"$SKILLS_SOURCE\":\"$SKILLS_TARGET\":ro)"), "deploy.json dev code-queue commit must bind D601 host skills into provider dev containers", {
|
|
commit: devCodeQueueCommit,
|
|
});
|
|
assertCondition(pinnedProviderRuntime.includes("-e UNIDESK_SKILLS_PATH=\"$SKILLS_TARGET\""), "deploy.json dev code-queue commit must pass target skills env into provider dev containers", {
|
|
commit: devCodeQueueCommit,
|
|
});
|
|
|
|
configureProviderRuntime({
|
|
config: {
|
|
codexHome: "/var/lib/unidesk/code-queue/codex-home",
|
|
defaultWorkdir: "/workspace",
|
|
devContainerDefaultProviderId: "D601",
|
|
devContainerImage: "unidesk-code-queue:d601",
|
|
devContainerMasterHost: "74.48.78.17",
|
|
devContainerWorkdir: "/home/ubuntu",
|
|
executionProviderIds: ["D601"],
|
|
mainProviderId: "D601-main",
|
|
remoteCodexEnvKeys: [],
|
|
remoteDefaultWorkdir: "/home/ubuntu",
|
|
runnerSkillsSourcePath: "/home/ubuntu/.agents/skills",
|
|
skillsPath: "/root/.agents/skills",
|
|
sourceCodexConfig: "/root/.codex/config.toml",
|
|
windowsNativeCodexBridgeDir: "/home/ubuntu/.unidesk/code-queue/windows-native-codex",
|
|
windowsNativeCodexCommand: "codex app-server --listen stdio://",
|
|
windowsNativeCodexConnectHost: "host.docker.internal",
|
|
windowsNativeCodexDefaultWorkdir: "/mnt/f/Work/ConStart",
|
|
windowsNativeCodexIdleTimeoutMs: 600_000,
|
|
},
|
|
safePreview: (value: string, max = 1000) => value.slice(0, max),
|
|
});
|
|
const devContainerPlan = buildDevContainerPlan("D601", { workdir: "/home/ubuntu" });
|
|
const devContainerStartScript = providerRuntimeForTest.remoteContainerStartScript(devContainerPlan, false);
|
|
assertCondition(devContainerStartScript.includes("SKILLS_SOURCE='/home/ubuntu/.agents/skills'"), "provider dev container start must use the D601 host skills source", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes("SKILLS_TARGET='/root/.agents/skills'"), "provider dev container start must use the runner target skills path", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes('-v "$SKILLS_SOURCE":"$SKILLS_TARGET":ro'), "provider dev container must bind source skills read-only to target", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes('-e UNIDESK_SKILLS_PATH="$SKILLS_TARGET"'), "provider dev container must export UNIDESK_SKILLS_PATH in docker run", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes('test -r "$UNIDESK_SKILLS_PATH/docs-spec/SKILL.md"'), "provider dev container readiness must verify required docs-spec skill at target", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes('test -r "$UNIDESK_SKILLS_PATH/cli-spec/SKILL.md"'), "provider dev container readiness must verify required cli-spec skill at target", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes('test -r "$UNIDESK_SKILLS_PATH/frontend-design/SKILL.md"'), "provider dev container readiness must verify required frontend-design skill at target", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes("playwright-cli/SKILL.md") && devContainerStartScript.includes("playwright/SKILL.md"), "provider dev container readiness must accept the playwright-cli alias", devContainerStartScript);
|
|
assertCondition(devContainerStartScript.includes("reuse_ready") && devContainerStartScript.includes('test "$UNIDESK_SKILLS_PATH" = "/root/.agents/skills"'), "provider dev container reuse must revalidate target skills before keeping an old container", devContainerStartScript);
|
|
const remoteCodexCommand = providerRuntimeForTest.remoteAppServerCommand({
|
|
id: "task",
|
|
queueId: "default",
|
|
prompt: "",
|
|
status: "running",
|
|
cwd: "/home/ubuntu",
|
|
providerId: "D601",
|
|
model: "gpt-5.5",
|
|
executionMode: "default",
|
|
currentAttempt: 1,
|
|
attempts: 1,
|
|
maxAttempts: 1,
|
|
codexThreadId: null,
|
|
reasoningEffort: null,
|
|
createdAt: "2026-05-24T00:00:00.000Z",
|
|
updatedAt: "2026-05-24T00:00:00.000Z",
|
|
startedAt: "2026-05-24T00:00:00.000Z",
|
|
completedAt: null,
|
|
lastActivityAt: "2026-05-24T00:00:00.000Z",
|
|
branch: null,
|
|
commitSha: null,
|
|
title: null,
|
|
error: null,
|
|
judge: null,
|
|
outputSeq: 0,
|
|
outputs: [],
|
|
events: [],
|
|
metadata: {},
|
|
} as never);
|
|
assertCondition(remoteCodexCommand.includes("export UNIDESK_SKILLS_PATH=") && remoteCodexCommand.includes("/root/.agents/skills"), "remote codex app-server must receive the target skills env", remoteCodexCommand);
|
|
|
|
const available = collectSkillAvailability({
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: "/home/ubuntu/.agents/skills",
|
|
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
});
|
|
assertCondition(available.source === "/home/ubuntu/.agents/skills", "skill report must expose source");
|
|
assertCondition(available.target === "/home/ubuntu/.agents/skills", "skill report must expose target");
|
|
assertCondition(typeof available.resolvedPath === "string" && available.resolvedPath.length > 0, "skill report must expose resolved path", available);
|
|
assertCondition(asRecord(available.resolution, "available.resolution").passesToRunnerEnv === true, "skill report must expose runner env path resolution", available.resolution);
|
|
assertCondition(Array.isArray(available.requiredSkills) && available.requiredSkills.includes("docs-spec"), "skill report must expose requiredSkills");
|
|
assertCondition(Array.isArray(available.missingSkills), "skill report must expose missingSkills");
|
|
assertCondition(asRecord(available.version, "available.version").selectedFingerprint !== undefined, "skill report must expose selected skills fingerprint", available.version);
|
|
assertCondition(asRecord(available.version, "available.version").sourceLatestMtime !== undefined, "skill report must expose source skills mtime", available.version);
|
|
assertCondition(available.valuesPrinted === false, "skill report must declare valuesPrinted=false");
|
|
assertCondition(asRecord(available.pathSpelling, "pathSpelling").forbiddenPathMustNotBeUsed === true, "skill report must flag misspelled path risk without spreading the literal path");
|
|
assertCondition(!JSON.stringify(available).includes(forbiddenPathLiteral), "skill report must not propagate misspelled path literal");
|
|
assertCondition(!JSON.stringify(available).includes("GH_TOKEN"), "skill report must not include secret environment names unrelated to skills");
|
|
|
|
const tmpRoot = mkdtempSync(join(tmpdir(), "unidesk-codequeue-skills-"));
|
|
const fixtureSource = join(tmpRoot, "source");
|
|
const fixtureMissingTarget = join(tmpRoot, "target-missing");
|
|
const fixtureSymlinkTarget = join(tmpRoot, "target-symlink");
|
|
const fixtureMissingSource = join(tmpRoot, "source-missing");
|
|
mkdirSync(fixtureSource, { recursive: true });
|
|
createSkillSet(fixtureSource, ["docs-spec", "cli-spec"]);
|
|
symlinkSync(fixtureSource, fixtureSymlinkTarget, "dir");
|
|
|
|
const missingTargetWithSource = collectSkillAvailability({
|
|
source: fixtureSource,
|
|
target: fixtureMissingTarget,
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(missingTargetWithSource.ok === false, "source exists target missing must keep runner unavailable until target is projected", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.runnerUsable === false, "source must not be passed as the runner skills path when target is missing", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.contractOk === false, "missing target must mark target projection contract degraded", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.degraded === true, "missing target should remain degraded for host rollout", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.blocker === "skills-target-missing", "missing target should preserve target missing degraded reason", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.degradedReason === "skills-target-missing", "missing target should expose bounded degraded reason", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.resolvedPath === fixtureMissingTarget, "missing target should keep runner path at the expected target", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.resolvedPathSource === "missing", "missing target should not expose source fallback resolution", missingTargetWithSource);
|
|
assertCondition(missingTargetWithSource.skillCount === 0 && missingTargetWithSource.sourceSkillCount === 2 && missingTargetWithSource.targetSkillCount === 0, "missing target should expose bounded source and target counts", missingTargetWithSource);
|
|
assertCondition(asRecord(missingTargetWithSource.resolution, "missingTargetWithSource.resolution").runnerEnvValue === fixtureMissingTarget, "missing target should not pass source path to runner env", missingTargetWithSource.resolution);
|
|
assertCondition(asRecord(missingTargetWithSource.resolution, "missingTargetWithSource.resolution").hostRolloutRequired === true, "missing target should require host rollout repair", missingTargetWithSource.resolution);
|
|
|
|
const symlinkOk = collectSkillAvailability({
|
|
source: fixtureSource,
|
|
target: fixtureSymlinkTarget,
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(symlinkOk.ok === true && symlinkOk.contractOk === true, "target symlink to source should satisfy runner and contract", symlinkOk);
|
|
assertCondition(symlinkOk.resolvedPath === fixtureSymlinkTarget, "target symlink should keep target as runner path", symlinkOk);
|
|
assertCondition(symlinkOk.resolvedPathSource === "target-symlink", "target symlink should expose target-symlink source", symlinkOk);
|
|
assertCondition(symlinkOk.targetSymlink === true, "target symlink should be reported", symlinkOk);
|
|
assertCondition(asRecord(symlinkOk.resolution, "symlinkOk.resolution").hostRolloutRequired === false, "target symlink should not require rollout repair", symlinkOk.resolution);
|
|
|
|
const missingBoth = collectSkillAvailability({
|
|
source: fixtureMissingSource,
|
|
target: fixtureMissingTarget,
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(missingBoth.ok === false && missingBoth.runnerUsable === false, "missing source and target should fail runner availability", missingBoth);
|
|
assertCondition(missingBoth.blocker === "skills-source-and-target-missing", "missing both should expose dedicated blocker", missingBoth);
|
|
assertCondition(missingBoth.resolvedPathSource === "missing", "missing both should expose missing resolution", missingBoth);
|
|
|
|
const redactionProbe = collectSkillAvailability({
|
|
source: fixtureSource,
|
|
target: fixtureMissingTarget,
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(!JSON.stringify(redactionProbe).includes("ghp_"), "skill report must not include token-like values", redactionProbe);
|
|
assertCondition(!JSON.stringify(redactionProbe).includes("github_pat_"), "skill report must not include GitHub PAT-like values", redactionProbe);
|
|
rmSync(tmpRoot, { recursive: true, force: true });
|
|
|
|
const missing = collectSkillAvailability({
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: "/path/that/does/not/exist/for-code-queue-skills-test",
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(missing.ok === false, "approved source must not keep missing-target runner usable");
|
|
assertCondition(missing.runnerUsable === false, "missing target with approved source should expose runner unavailable");
|
|
assertCondition(missing.contractOk === false, "missing target with approved source should expose hostPath contract degraded");
|
|
assertCondition(missing.degraded === true, "missing target should be degraded");
|
|
assertCondition(missing.blocker === "skills-target-missing", "missing target should expose blocker", missing);
|
|
assertCondition(missing.targetMissingSkills.includes("docs-spec") && missing.targetMissingSkills.includes("cli-spec"), "missing target should list target missing skills", missing);
|
|
assertCondition(missing.resolvedPath === "/path/that/does/not/exist/for-code-queue-skills-test", "missing target should keep the configured target path", missing);
|
|
assertCondition(missing.resolvedPathSource === "missing", "missing target should not expose source fallback", missing);
|
|
assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false");
|
|
|
|
const typoTarget = collectSkillAvailability({
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: forbiddenTargetPath,
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(typoTarget.ok === false, "misspelled target should fail", typoTarget);
|
|
assertCondition(typoTarget.degraded === true, "misspelled target should be degraded", typoTarget);
|
|
assertCondition(typoTarget.blocker === "forbidden-skills-path-configured", "misspelled target should expose dedicated blocker", typoTarget);
|
|
assertCondition(asRecord(typoTarget.pathSpelling, "typoTarget.pathSpelling").forbiddenPathConfigured === true, "misspelled target should mark configured typo", typoTarget.pathSpelling);
|
|
assertCondition(JSON.stringify(asRecord(typoTarget.pathSpelling, "typoTarget.pathSpelling").forbiddenPathRoles).includes("target"), "misspelled target should classify target role", typoTarget.pathSpelling);
|
|
assertCondition(typoTarget.valuesPrinted === false, "misspelled target report must declare valuesPrinted=false");
|
|
|
|
const syncDryRun = collectSkillSyncPreflight({
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: "/path/that/does/not/exist/for-code-queue-skills-test",
|
|
requiredSkills: ["docs-spec", "cli-spec"],
|
|
});
|
|
assertCondition(syncDryRun.dryRun === true && syncDryRun.mutation === false, "skills sync contract must be dry-run and non-mutating", syncDryRun);
|
|
assertCondition(syncDryRun.syncMode === "hostPath-read-only-projection", "skills sync must describe the hostPath projection lifecycle", syncDryRun);
|
|
assertCondition(syncDryRun.source.path === "/home/ubuntu/.agents/skills", "skills sync must expose source", syncDryRun.source);
|
|
assertCondition(syncDryRun.target.path === "/path/that/does/not/exist/for-code-queue-skills-test", "skills sync must expose target", syncDryRun.target);
|
|
assertCondition(syncDryRun.expected.source === "/home/ubuntu/.agents/skills", "skills sync must expose stable expected source", syncDryRun.expected);
|
|
assertCondition(syncDryRun.expected.target === "/root/.agents/skills", "skills sync must expose stable expected target", syncDryRun.expected);
|
|
assertCondition(syncDryRun.expected.env === "UNIDESK_SKILLS_PATH" && syncDryRun.expected.envValue === "/root/.agents/skills", "skills sync must expose env contract", syncDryRun.expected);
|
|
assertCondition(syncDryRun.counts.requiredSkills === 2, "skills sync must expose required skill count", syncDryRun.counts);
|
|
assertCondition(syncDryRun.counts.targetSkills === 0 && syncDryRun.counts.missingTargetSkills === 2, "skills sync must expose target counts and missing count", syncDryRun.counts);
|
|
assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").sourceFingerprint !== undefined, "skills sync must expose source fingerprint", syncDryRun.version);
|
|
assertCondition(asRecord(syncDryRun.version, "syncDryRun.version").targetLatestMtime !== undefined, "skills sync must expose target mtime", syncDryRun.version);
|
|
assertCondition(syncDryRun.missing.targetSkills.includes("docs-spec") && syncDryRun.missing.targetSkills.includes("cli-spec"), "skills sync must expose missing target skills", syncDryRun.missing);
|
|
assertCondition(syncDryRun.blocker === "unapproved-target", "arbitrary target paths must be blocked before silent copying", syncDryRun);
|
|
assertCondition(syncDryRun.plannedActions.copy === false && syncDryRun.plannedActions.copyFromArbitraryPath === false, "skills sync dry-run must not plan arbitrary copy", syncDryRun.plannedActions);
|
|
assertCondition(syncDryRun.plannedActions.restartRequired === false && syncDryRun.plannedActions.readsSecrets === false, "skills sync dry-run must not require restart or read secrets", syncDryRun.plannedActions);
|
|
assertCondition(Array.isArray(syncDryRun.instructions) && syncDryRun.instructions.some((item) => item.includes("read-only hostPath projection")), "skills sync must include lifecycle instructions", syncDryRun.instructions);
|
|
assertCondition(syncDryRun.valuesPrinted === false, "skills sync must declare valuesPrinted=false", syncDryRun);
|
|
assertCondition(!JSON.stringify(syncDryRun).includes(forbiddenPathLiteral), "skills sync report must not propagate misspelled path literal");
|
|
|
|
const missingTargetSync = collectSkillSyncPreflight({ target: "/path/that/does/not/exist/for-code-queue-skills-test" });
|
|
assertCondition(missingTargetSync.blocker === "unapproved-target", "non-default target must be rejected as unapproved", missingTargetSync);
|
|
|
|
const typoTargetSync = collectSkillSyncPreflight({ target: forbiddenTargetPath });
|
|
assertCondition(typoTargetSync.blocker === "forbidden-skills-path-configured", "misspelled sync target must be rejected as a typo before generic target approval", typoTargetSync);
|
|
assertCondition(asRecord(typoTargetSync.pathSpelling, "typoTargetSync.pathSpelling").forbiddenPathConfigured === true, "misspelled sync target should mark configured typo", typoTargetSync.pathSpelling);
|
|
assertCondition(JSON.stringify(asRecord(typoTargetSync.pathSpelling, "typoTargetSync.pathSpelling").forbiddenPathRoles).includes("target"), "misspelled sync target should classify target role", typoTargetSync.pathSpelling);
|
|
assertCondition(typoTargetSync.valuesPrinted === false, "misspelled sync target must declare valuesPrinted=false");
|
|
|
|
const typoSourceSync = collectSkillSyncPreflight({ source: forbiddenSourcePath });
|
|
assertCondition(typoSourceSync.blocker === "forbidden-skills-path-configured", "misspelled sync source must be rejected as a typo before generic source approval", typoSourceSync);
|
|
assertCondition(asRecord(typoSourceSync.pathSpelling, "typoSourceSync.pathSpelling").forbiddenPathConfigured === true, "misspelled sync source should mark configured typo", typoSourceSync.pathSpelling);
|
|
assertCondition(JSON.stringify(asRecord(typoSourceSync.pathSpelling, "typoSourceSync.pathSpelling").forbiddenPathRoles).includes("source"), "misspelled sync source should classify source role", typoSourceSync.pathSpelling);
|
|
|
|
assertCondition(runtimePreflight.includes("skills: SkillAvailabilityReport"), "runtime preflight type must include skills report");
|
|
assertCondition(runtimePreflight.includes("skillsSync: SkillSyncPreflightReport"), "runtime preflight type must include skills sync report");
|
|
assertCondition(runtimePreflight.includes("collectSkillAvailability"), "runtime preflight must collect skills availability");
|
|
assertCondition(runtimePreflight.includes("collectSkillSyncPreflight"), "runtime preflight must collect skills sync preflight");
|
|
assertCondition(runtimePreflight.includes("skills.contractOk && ports.codex.ok"), "runtime preflight ok must depend on the read-only target projection contract");
|
|
assertCondition(indexSource.includes("skills.contractOk === true"), "dev-ready must gate on structured target projection contract");
|
|
assertCondition(indexSource.includes("resolvedRunnerSkillsPath"), "runtime must pass resolved skills path to code agents");
|
|
assertCondition(indexSource.includes("runnerSkillsBlocker"), "scheduler must check skills before starting code agents");
|
|
assertCondition(indexSource.includes("task_blocked_by_runner_skills"), "scheduler must emit structured runner skills blockers");
|
|
assertCondition(indexSource.includes("runnerDisposition: \"infra-blocked\""), "runner skills blocker must classify infra-blocked");
|
|
assertCondition(indexSource.includes("collectSkillsSyncPreflight"), "runtime index must expose skills sync preflight");
|
|
assertCondition(indexSource.includes("/api/skills-sync"), "runtime must expose a dry-run skills sync endpoint");
|
|
assertCondition(indexSource.includes("pass dryRun=1"), "skills sync endpoint must reject non-dry-run calls");
|
|
assertCondition(codeQueueCli.includes("failureKind: \"dry-run-required\""), "codex skills-sync CLI must require --dry-run with structured output");
|
|
assertCondition(codeQueueCli.includes("codex skills-sync is dry-run only; pass --dry-run"), "codex skills-sync CLI must explain the dry-run requirement");
|
|
assertCondition(codeQueueCli.includes("Code Queue skills sync dry-run could not reach the control plane"), "codex skills-sync CLI must return structured control-plane failure output");
|
|
assertCondition(codeQueueCli.includes("compact-skills-sync-control-plane-failure"), "codex skills-sync CLI must keep control-plane failure output compact");
|
|
assertCondition(codeQueueCli.includes("compactSkillsSyncStatus"), "codex CLI must compact skills sync output");
|
|
assertCondition(codeQueueCli.includes("runner-skills-blocker"), "codex preflight must classify skill lifecycle blockers");
|
|
assertCondition(codeQueueCli.includes("forbiddenPathConfigured"), "codex CLI must preserve configured typo classification in compact output");
|
|
assertCondition(microserviceCli.includes("compactSkillSync"), "microservice health summary must compact skills sync output");
|
|
assertCondition(microserviceCli.includes("forbiddenPathConfigured"), "microservice health summary must preserve configured typo classification");
|
|
assertCondition(helpSource.includes("codex skills-sync --dry-run"), "CLI help must document the skills sync dry-run command");
|
|
assertCondition(docsReference.includes("codex skills-sync --dry-run"), "reference docs must document the skills sync dry-run command");
|
|
assertCondition(docsReference.includes("forbidden-skills-path-configured"), "reference docs must document configured typo blocker");
|
|
|
|
const skillsPreflightTransport = {
|
|
config: null,
|
|
coreFetch: () => ({
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
runtimePreflight: {
|
|
ok: false,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
cwd: "/workspace/unidesk",
|
|
pid: 601,
|
|
skills: missing,
|
|
skillsSync: syncDryRun,
|
|
ports: {},
|
|
pullRequestDelivery: {
|
|
ok: true,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
tools: {},
|
|
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true },
|
|
authBroker: { ok: true, configured: true, source: "auth-broker" },
|
|
credentials: {
|
|
ghTokenPresent: false,
|
|
githubTokenPresent: false,
|
|
ghHostsConfigPresent: false,
|
|
gitCredentialsPresent: false,
|
|
},
|
|
git: {
|
|
insideWorktree: true,
|
|
branch: "code-queue/issue-68-runner-skills-lifecycle",
|
|
head: "abc1234",
|
|
originMaster: "def5678",
|
|
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
|
|
home: "/root",
|
|
homeWritable: true,
|
|
knownHostsPresent: true,
|
|
privateKeyPresent: true,
|
|
},
|
|
githubContext: {
|
|
host: "github.com",
|
|
apiBaseUrl: "https://api.github.com",
|
|
repo: "pikasTech/unidesk",
|
|
issueProbeNumber: 68,
|
|
},
|
|
egress: { proxy: {} },
|
|
remote: null,
|
|
limitations: [],
|
|
risks: [],
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
const defaultPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], skillsPreflightTransport), "default preflight summary");
|
|
assertCondition(defaultPreflightSummary.failureKind === "runner-skills-blocker", "missing target should classify as runner skills blocker even when source exists", defaultPreflightSummary);
|
|
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").hostRolloutRequired === true, "default preflight should expose host rollout blocker separately", defaultPreflightSummary);
|
|
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").source === missing.source, "default preflight should expose skills source in the bounded contract", defaultPreflightSummary.skillsContract);
|
|
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").target === missing.target, "default preflight should expose skills target in the bounded contract", defaultPreflightSummary.skillsContract);
|
|
assertCondition(Array.isArray(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").requiredSkills), "default preflight should expose requiredSkills in the bounded contract", defaultPreflightSummary.skillsContract);
|
|
assertCondition(Array.isArray(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").missingSkills), "default preflight should expose missingSkills in the bounded contract", defaultPreflightSummary.skillsContract);
|
|
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").repairHint !== null, "default preflight should expose repairHint in the bounded contract", defaultPreflightSummary.skillsContract);
|
|
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").valuesPrinted === false, "default preflight skills contract must declare valuesPrinted=false", defaultPreflightSummary.skillsContract);
|
|
assertCondition(defaultPreflightSummary.preflight === undefined, "default PR preflight should omit detailed preflight internals", defaultPreflightSummary);
|
|
assertCondition(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").fullDetailOmitted === true, "default PR preflight should disclose full detail omission", defaultPreflightSummary.disclosure);
|
|
assertCondition(String(asRecord(defaultPreflightSummary.disclosure, "defaultPreflightSummary.disclosure").expandWith ?? "").includes("--full"), "default PR preflight should point to --full expansion", defaultPreflightSummary.disclosure);
|
|
|
|
const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote", "--full"], skillsPreflightTransport), "full preflight summary");
|
|
const preflight = asRecord(preflightSummary.preflight, "preflight");
|
|
const preflightSkills = asRecord(preflight.skills, "preflight.skills");
|
|
const preflightSkillsSync = asRecord(preflight.skillsSync, "preflight.skillsSync");
|
|
assertCondition(preflightSummary.failureKind === "runner-skills-blocker", "full preflight should classify missing target as runner blocker", preflightSummary);
|
|
assertCondition(asRecord(preflightSummary.skillsContract, "preflightSummary.skillsContract").degradedReason === "skills-target-missing", "full preflight should expose target missing as contract degraded reason", preflightSummary);
|
|
assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must show skills target", preflightSkills);
|
|
assertCondition(preflightSkills.resolvedPath === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must keep resolved path at the target", preflightSkills);
|
|
assertCondition(preflightSkills.resolvedPathSource === "missing", "full preflight must not show source fallback resolution", preflightSkills);
|
|
assertCondition(preflightSkillsSync.dryRun === true && preflightSkillsSync.mutation === false, "full preflight must show non-mutating skills sync dry-run", preflightSkillsSync);
|
|
assertCondition(asRecord(preflightSkillsSync.counts, "preflight.skillsSync.counts").missingTargetSkills === 2, "full preflight must show missing target count", preflightSkillsSync);
|
|
assertCondition(asRecord(preflightSkillsSync.plannedActions, "preflight.skillsSync.plannedActions").copy === false, "full preflight must show no copy action", preflightSkillsSync);
|
|
assertCondition(preflightSkillsSync.valuesPrinted === false, "full preflight skills sync must declare valuesPrinted=false", preflightSkillsSync);
|
|
assertCondition(!JSON.stringify(preflightSkillsSync).includes(forbiddenPathLiteral), "full preflight must not propagate misspelled path literal");
|
|
|
|
const legacyRuntimeSkills = {
|
|
ok: false,
|
|
runnerUsable: false,
|
|
contractOk: false,
|
|
path: "/root/.agents/skills",
|
|
resolvedPath: "/root/.agents/skills",
|
|
resolvedPathSource: null,
|
|
resolution: null,
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: "/root/.agents/skills",
|
|
exists: false,
|
|
available: false,
|
|
degraded: true,
|
|
blocker: "skills-target-missing",
|
|
degradedReason: "skills-target-missing",
|
|
readonly: false,
|
|
skillCount: 0,
|
|
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
missingSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
valuesPrinted: false,
|
|
pathSpelling: {
|
|
expectedTarget: "/root/.agents/skills",
|
|
forbiddenPathChecked: true,
|
|
forbiddenPathExists: false,
|
|
forbiddenPathConfigured: false,
|
|
forbiddenPathRoles: [],
|
|
forbiddenPathMustNotBeUsed: true,
|
|
},
|
|
repairHint: "Mount /home/ubuntu/.agents/skills read-only at /root/.agents/skills, set UNIDESK_SKILLS_PATH=/root/.agents/skills, and remove any forbidden skills path spelling.",
|
|
};
|
|
const legacyRuntimeSkillsSync = {
|
|
ok: false,
|
|
degraded: true,
|
|
blocker: "skills-target-missing",
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
mode: "dry-run",
|
|
dryRun: true,
|
|
mutation: false,
|
|
syncMode: "hostPath-read-only-projection",
|
|
source: {
|
|
path: "/home/ubuntu/.agents/skills",
|
|
approved: true,
|
|
exists: true,
|
|
directory: true,
|
|
readable: true,
|
|
writable: true,
|
|
readonly: false,
|
|
mountPoint: "/home/ubuntu",
|
|
symlink: false,
|
|
realPath: null,
|
|
skillCount: 49,
|
|
version: null,
|
|
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
missingSkills: [],
|
|
error: null,
|
|
},
|
|
target: {
|
|
path: "/root/.agents/skills",
|
|
approved: true,
|
|
exists: false,
|
|
directory: false,
|
|
readable: false,
|
|
writable: false,
|
|
readonly: false,
|
|
mountPoint: "/",
|
|
symlink: false,
|
|
realPath: null,
|
|
skillCount: 0,
|
|
version: null,
|
|
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
missingSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
error: null,
|
|
},
|
|
expected: {
|
|
source: "/home/ubuntu/.agents/skills",
|
|
target: "/root/.agents/skills",
|
|
env: "UNIDESK_SKILLS_PATH",
|
|
envValue: "/root/.agents/skills",
|
|
mount: "/home/ubuntu/.agents/skills mounted read-only to /root/.agents/skills",
|
|
requiredSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
},
|
|
counts: {
|
|
sourceSkills: 49,
|
|
targetSkills: 0,
|
|
requiredSkills: 4,
|
|
missingSourceSkills: 0,
|
|
missingTargetSkills: 4,
|
|
},
|
|
version: null,
|
|
missing: {
|
|
sourceSkills: [],
|
|
targetSkills: ["docs-spec", "cli-spec", "frontend-design", "playwright-cli"],
|
|
},
|
|
permissionFailures: [],
|
|
pathSpelling: {
|
|
expectedTarget: "/root/.agents/skills",
|
|
forbiddenPathChecked: true,
|
|
forbiddenPathExists: false,
|
|
forbiddenPathConfigured: false,
|
|
forbiddenPathRoles: [],
|
|
forbiddenPathMustNotBeUsed: true,
|
|
},
|
|
plannedActions: {
|
|
copy: false,
|
|
writesSource: false,
|
|
writesTarget: false,
|
|
restartRequired: false,
|
|
readsSecrets: false,
|
|
copyFromArbitraryPath: false,
|
|
},
|
|
commands: {
|
|
dryRun: "bun scripts/cli.ts codex skills-sync --dry-run",
|
|
full: "bun scripts/cli.ts codex skills-sync --dry-run --full",
|
|
health: "bun scripts/cli.ts microservice health code-queue",
|
|
runtimePreflight: "bun scripts/cli.ts codex pr-preflight --remote",
|
|
contractTest: "bun scripts/code-queue-runner-skills-contract-test.ts",
|
|
},
|
|
valuesPrinted: false,
|
|
};
|
|
const legacyRuntimePreflightTransport = {
|
|
config: null,
|
|
coreFetch: () => ({
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
runtimePreflight: {
|
|
ok: false,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
cwd: "/workspace/unidesk",
|
|
pid: 601,
|
|
skills: legacyRuntimeSkills,
|
|
skillsSync: legacyRuntimeSkillsSync,
|
|
ports: {},
|
|
pullRequestDelivery: {
|
|
ok: true,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
tools: {},
|
|
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true },
|
|
authBroker: { ok: true, configured: true, source: "auth-broker" },
|
|
credentials: {
|
|
ghTokenPresent: true,
|
|
githubTokenPresent: false,
|
|
ghHostsConfigPresent: false,
|
|
gitCredentialsPresent: false,
|
|
},
|
|
git: {
|
|
insideWorktree: true,
|
|
branch: "code-queue/issue-68-runner-skills-lifecycle",
|
|
head: "abc1234",
|
|
originMaster: "def5678",
|
|
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
|
|
home: "/root",
|
|
homeWritable: true,
|
|
knownHostsPresent: true,
|
|
privateKeyPresent: true,
|
|
},
|
|
githubContext: {
|
|
host: "github.com",
|
|
apiBaseUrl: "https://api.github.com",
|
|
repo: "pikasTech/unidesk",
|
|
issueProbeNumber: 68,
|
|
},
|
|
egress: { proxy: {} },
|
|
remote: null,
|
|
limitations: [],
|
|
risks: [],
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
const legacyDefaultPreflight = asRecord(codexPrPreflightQueryForTest(["--remote"], legacyRuntimePreflightTransport), "legacy default preflight");
|
|
const legacySkillsContract = asRecord(legacyDefaultPreflight.skillsContract, "legacyDefaultPreflight.skillsContract");
|
|
assertCondition(legacyDefaultPreflight.failureKind === "runner-skills-blocker", "legacy runtime shape should still classify missing target as runner skills blocker", legacyDefaultPreflight);
|
|
assertCondition(legacySkillsContract.source === "/home/ubuntu/.agents/skills", "legacy runtime shape should expose source path from skillsSync", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.target === "/root/.agents/skills", "legacy runtime shape should expose target path from skillsSync", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.hostRolloutRequired === true, "legacy runtime shape with source available and target missing should require host rollout", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.degradedReason === "skills-target-missing", "legacy runtime shape should keep actionable degraded reason", legacySkillsContract);
|
|
assertCondition(Array.isArray(legacySkillsContract.requiredSkills) && legacySkillsContract.requiredSkills.includes("docs-spec"), "legacy runtime shape should expose requiredSkills", legacySkillsContract);
|
|
assertCondition(Array.isArray(legacySkillsContract.missingSkills) && legacySkillsContract.missingSkills.includes("docs-spec"), "legacy runtime shape should expose missingSkills", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.sourceSkillCount === 49 && legacySkillsContract.targetSkillCount === 0, "legacy runtime shape should expose source/target skill counts", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.repairHint !== null, "legacy runtime shape should expose repairHint", legacySkillsContract);
|
|
assertCondition(legacySkillsContract.valuesPrinted === false, "legacy runtime shape contract must declare valuesPrinted=false", legacySkillsContract);
|
|
|
|
const typoPreflightTransport = {
|
|
config: null,
|
|
coreFetch: () => ({
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
runtimePreflight: {
|
|
ok: false,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
cwd: "/workspace/unidesk",
|
|
pid: 601,
|
|
skills: typoTarget,
|
|
skillsSync: typoTargetSync,
|
|
ports: {},
|
|
pullRequestDelivery: {
|
|
ok: true,
|
|
checkedAt: "2026-05-23T00:00:00.000Z",
|
|
tools: {},
|
|
unideskGhCli: { ok: true, path: "/workspace/unidesk/scripts/cli.ts", present: true },
|
|
authBroker: { ok: true, configured: true, source: "auth-broker" },
|
|
credentials: {
|
|
ghTokenPresent: true,
|
|
githubTokenPresent: false,
|
|
ghHostsConfigPresent: false,
|
|
gitCredentialsPresent: false,
|
|
},
|
|
git: {
|
|
insideWorktree: true,
|
|
branch: "code-queue/issue-68-runner-skills-lifecycle",
|
|
head: "abc1234",
|
|
originMaster: "def5678",
|
|
remoteOrigin: "git@github.com:pikasTech/unidesk.git",
|
|
home: "/root",
|
|
homeWritable: true,
|
|
knownHostsPresent: true,
|
|
privateKeyPresent: true,
|
|
},
|
|
githubContext: {
|
|
host: "github.com",
|
|
apiBaseUrl: "https://api.github.com",
|
|
repo: "pikasTech/unidesk",
|
|
issueProbeNumber: 68,
|
|
},
|
|
egress: { proxy: {} },
|
|
remote: null,
|
|
limitations: [],
|
|
risks: [],
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
};
|
|
const typoPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote"], typoPreflightTransport), "typo preflight summary");
|
|
assertCondition(typoPreflightSummary.failureKind === "runner-skills-blocker", "typo preflight should classify configured typo as runner skills blocker", typoPreflightSummary);
|
|
assertCondition(typoPreflightSummary.degradedReason === "forbidden-skills-path-configured", "typo preflight degraded reason should preserve configured typo blocker", typoPreflightSummary);
|
|
assertCondition(typoPreflightSummary.preflight === undefined, "typo default preflight should remain bounded", typoPreflightSummary);
|
|
const typoFullPreflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote", "--full"], typoPreflightTransport), "typo full preflight summary");
|
|
const typoFullPreflight = asRecord(typoFullPreflightSummary.preflight, "typoFullPreflightSummary.preflight");
|
|
const typoPreflightSkills = asRecord(typoFullPreflight.skills, "typoFullPreflight.skills");
|
|
const typoPreflightPathSpelling = asRecord(typoPreflightSkills.pathSpelling, "typoFullPreflight.skills.pathSpelling");
|
|
assertCondition(typoPreflightPathSpelling.forbiddenPathConfigured === true, "typo full preflight output must expose configured typo classification", typoPreflightPathSpelling);
|
|
|
|
const healthSummary = asRecord(summarizeMicroserviceObservation("health", "code-queue", {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
ok: false,
|
|
service: "code-queue",
|
|
skills: missing,
|
|
skillsSync: syncDryRun,
|
|
},
|
|
}, []), "microservice health summary");
|
|
const microservice = asRecord(healthSummary.microservice, "microservice");
|
|
const healthCompact = asRecord(microservice.summary, "microservice.summary");
|
|
const healthSkills = asRecord(healthCompact.skills, "microservice.summary.skills");
|
|
const healthSkillsSync = asRecord(healthCompact.skillsSync, "microservice.summary.skillsSync");
|
|
assertCondition(healthSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "compact health must show skills target", healthSkills);
|
|
assertCondition(healthSkillsSync.dryRun === true && healthSkillsSync.mutation === false, "compact health must show dry-run skills sync", healthSkillsSync);
|
|
assertCondition(asRecord(healthSkillsSync.counts, "microservice.summary.skillsSync.counts").missingTargetSkills === 2, "compact health must show missing target count", healthSkillsSync);
|
|
assertCondition(asRecord(healthSkillsSync.plannedActions, "microservice.summary.skillsSync.plannedActions").copyFromArbitraryPath === false, "compact health must show arbitrary copy is blocked", healthSkillsSync);
|
|
assertCondition(!JSON.stringify(healthSkillsSync).includes(forbiddenPathLiteral), "compact health must not propagate misspelled path literal");
|
|
|
|
const typoHealthSummary = asRecord(summarizeMicroserviceObservation("health", "code-queue", {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
ok: false,
|
|
service: "code-queue",
|
|
skills: typoTarget,
|
|
skillsSync: typoTargetSync,
|
|
},
|
|
}, []), "microservice typo health summary");
|
|
const typoHealthSkills = asRecord(asRecord(asRecord(typoHealthSummary.microservice, "typoHealthSummary.microservice").summary, "typoHealthSummary.microservice.summary").skills, "typoHealthSummary.skills");
|
|
const typoHealthPathSpelling = asRecord(typoHealthSkills.pathSpelling, "typoHealthSummary.skills.pathSpelling");
|
|
assertCondition(typoHealthSkills.blocker === "forbidden-skills-path-configured", "compact health must preserve configured typo blocker", typoHealthSkills);
|
|
assertCondition(typoHealthPathSpelling.forbiddenPathConfigured === true, "compact health must expose configured typo classification", typoHealthPathSpelling);
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills",
|
|
"provider dev containers bind /home/ubuntu/.agents/skills read-only at /root/.agents/skills and pass UNIDESK_SKILLS_PATH to Codex/OpenCode",
|
|
"deploy.json dev Code Queue pins a commit whose manifest and runtime require the target skills projection",
|
|
"skill availability report exposes source, target, requiredSkills, missingSkills, version fingerprint/mtime, degraded/blocker and valuesPrinted=false",
|
|
"skills sync dry-run reports source, target, counts, version fingerprint/mtime, missing skills, permission failures, instructions and no-copy actions",
|
|
"scheduler blocks runner startup with structured infra-blocked output when required skills are unavailable",
|
|
"runtime-preflight, dev-ready, health and PR preflight require the target projection instead of source fallback",
|
|
"default health/preflight summaries expose bounded skills lifecycle evidence and --full expansion",
|
|
"misspelled skills paths are rejected with forbidden-skills-path-configured before generic missing/unapproved path blockers",
|
|
],
|
|
observedRunner: {
|
|
source: available.source,
|
|
target: available.target,
|
|
ok: available.ok,
|
|
missingSkills: available.missingSkills,
|
|
syncDryRunOk: syncDryRun.ok,
|
|
syncDryRunBlocker: syncDryRun.blocker,
|
|
valuesPrinted: available.valuesPrinted,
|
|
},
|
|
}, null, 2)}\n`);
|