233 lines
18 KiB
TypeScript
233 lines
18 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability";
|
|
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;
|
|
}
|
|
|
|
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 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("");
|
|
|
|
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, "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");
|
|
|
|
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(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(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 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, "missing target should fail");
|
|
assertCondition(missing.degraded === true, "missing target should be degraded");
|
|
assertCondition(missing.blocker === "skills-target-missing", "missing target should expose blocker", missing);
|
|
assertCondition(missing.missingSkills.includes("docs-spec") && missing.missingSkills.includes("cli-spec"), "missing target should list required missing skills", missing);
|
|
assertCondition(missing.valuesPrinted === false, "missing report must also 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(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);
|
|
|
|
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.ok && skillsSync.ok && ports.codex.ok"), "runtime preflight ok must depend on skills and skillsSync");
|
|
assertCondition(indexSource.includes("const skillsReady = skills.ok === true"), "dev-ready must gate on structured skills ok");
|
|
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(microserviceCli.includes("compactSkillSync"), "microservice health summary must compact skills sync output");
|
|
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");
|
|
|
|
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", "default preflight should classify missing skills as runner-skills-blocker", defaultPreflightSummary);
|
|
assertCondition(defaultPreflightSummary.degradedReason === "unapproved-target", "default preflight degraded reason should use the skills sync blocker first", defaultPreflightSummary);
|
|
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 skills as runner-skills-blocker", preflightSummary);
|
|
assertCondition(preflightSummary.degradedReason === "unapproved-target", "full preflight degraded reason should use the skills sync blocker first", preflightSummary);
|
|
assertCondition(preflightSkills.target === "/path/that/does/not/exist/for-code-queue-skills-test", "full preflight must show skills target", 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 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");
|
|
|
|
process.stdout.write(`${JSON.stringify({
|
|
ok: true,
|
|
checks: [
|
|
"production Code Queue mounts /home/ubuntu/.agents/skills read-only at /root/.agents/skills",
|
|
"skill availability report exposes source, target, requiredSkills, missingSkills, degraded/blocker and valuesPrinted=false",
|
|
"skills sync dry-run reports source, target, counts, missing skills, permission failures, instructions and no-copy actions",
|
|
"runtime-preflight, dev-ready, health and PR preflight use the same structured skill and sync reports",
|
|
"default health/preflight summaries expose bounded skills lifecycle evidence and --full expansion",
|
|
"misspelled skills paths are only surfaced as a forbidden diagnostic risk",
|
|
],
|
|
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`);
|