fix: resolve code queue runner skills path
Resolve Code Queue runner skills through approved source fallback while preserving hostPath contract diagnostics.
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { collectSkillAvailability, collectSkillSyncPreflight } from "../src/components/microservices/code-queue/src/skill-availability";
|
||||
import { codexPrPreflightQueryForTest } from "./src/code-queue";
|
||||
import { summarizeMicroserviceObservation } from "./src/microservices";
|
||||
@@ -18,6 +20,14 @@ function countOccurrences(haystack: string, needle: string): number {
|
||||
return haystack.split(needle).length - 1;
|
||||
}
|
||||
|
||||
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 runtimePreflight = readFileSync("src/components/microservices/code-queue/src/runtime-preflight.ts", "utf8");
|
||||
@@ -58,6 +68,8 @@ const available = collectSkillAvailability({
|
||||
});
|
||||
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(available.valuesPrinted === false, "skill report must declare valuesPrinted=false");
|
||||
@@ -65,15 +77,74 @@ assertCondition(asRecord(available.pathSpelling, "pathSpelling").forbiddenPathMu
|
||||
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 sourceFallback = collectSkillAvailability({
|
||||
source: fixtureSource,
|
||||
target: fixtureMissingTarget,
|
||||
requiredSkills: ["docs-spec", "cli-spec"],
|
||||
});
|
||||
assertCondition(sourceFallback.ok === true, "source exists target missing should keep runner usable", sourceFallback);
|
||||
assertCondition(sourceFallback.runnerUsable === true, "source fallback should mark runner usable", sourceFallback);
|
||||
assertCondition(sourceFallback.contractOk === false, "source fallback should still mark target projection contract degraded", sourceFallback);
|
||||
assertCondition(sourceFallback.degraded === true, "source fallback should remain degraded for host rollout", sourceFallback);
|
||||
assertCondition(sourceFallback.blocker === "skills-target-missing", "source fallback should preserve target missing degraded reason", sourceFallback);
|
||||
assertCondition(sourceFallback.degradedReason === "skills-target-missing", "source fallback should expose bounded degraded reason", sourceFallback);
|
||||
assertCondition(sourceFallback.resolvedPath === fixtureSource, "source fallback should resolve to source path", sourceFallback);
|
||||
assertCondition(sourceFallback.resolvedPathSource === "source-fallback", "source fallback should expose resolved path source", sourceFallback);
|
||||
assertCondition(sourceFallback.skillCount === 2 && sourceFallback.sourceSkillCount === 2 && sourceFallback.targetSkillCount === 0, "source fallback should expose bounded counts", sourceFallback);
|
||||
assertCondition(asRecord(sourceFallback.resolution, "sourceFallback.resolution").runnerEnvValue === fixtureSource, "source fallback should pass resolved path to runner env", sourceFallback.resolution);
|
||||
assertCondition(asRecord(sourceFallback.resolution, "sourceFallback.resolution").hostRolloutRequired === true, "source fallback should require host rollout repair", sourceFallback.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, "missing target should fail");
|
||||
assertCondition(missing.ok === true, "approved source should keep missing-target runner usable");
|
||||
assertCondition(missing.runnerUsable === true, "missing target with approved source should expose runner usable");
|
||||
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.missingSkills.includes("docs-spec") && missing.missingSkills.includes("cli-spec"), "missing target should list required missing skills", missing);
|
||||
assertCondition(missing.targetMissingSkills.includes("docs-spec") && missing.targetMissingSkills.includes("cli-spec"), "missing target should list target missing skills", missing);
|
||||
assertCondition(missing.resolvedPath === "/home/ubuntu/.agents/skills", "missing target should resolve to approved source", missing);
|
||||
assertCondition(missing.resolvedPathSource === "source-fallback", "missing target should expose source fallback", missing);
|
||||
assertCondition(missing.valuesPrinted === false, "missing report must also declare valuesPrinted=false");
|
||||
|
||||
const typoTarget = collectSkillAvailability({
|
||||
@@ -128,8 +199,9 @@ assertCondition(runtimePreflight.includes("skills: SkillAvailabilityReport"), "r
|
||||
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(runtimePreflight.includes("skills.runnerUsable && ports.codex.ok"), "runtime preflight ok must depend on runner usable skills without blocking on host rollout contract drift");
|
||||
assertCondition(indexSource.includes("skills.runnerUsable === true"), "dev-ready must gate on structured runner usable skills");
|
||||
assertCondition(indexSource.includes("resolvedRunnerSkillsPath"), "runtime must pass resolved skills path to code agents");
|
||||
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");
|
||||
@@ -199,8 +271,8 @@ const skillsPreflightTransport = {
|
||||
}),
|
||||
};
|
||||
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.failureKind !== "runner-skills-blocker", "source fallback should not classify as runner skills blocker", defaultPreflightSummary);
|
||||
assertCondition(asRecord(defaultPreflightSummary.skillsContract, "defaultPreflightSummary.skillsContract").hostRolloutRequired === true, "default preflight should expose host rollout blocker separately", 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);
|
||||
@@ -209,9 +281,11 @@ const preflightSummary = asRecord(codexPrPreflightQueryForTest(["--remote", "--f
|
||||
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(preflightSummary.failureKind !== "runner-skills-blocker", "full preflight should keep source fallback out of runner blocker classification", 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 === "/home/ubuntu/.agents/skills", "full preflight must show resolved source fallback path", preflightSkills);
|
||||
assertCondition(preflightSkills.resolvedPathSource === "source-fallback", "full preflight must 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);
|
||||
|
||||
Reference in New Issue
Block a user