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);
|
||||
|
||||
@@ -6078,7 +6078,12 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
|
||||
if (record === null) return null;
|
||||
return {
|
||||
ok: record.ok ?? false,
|
||||
runnerUsable: record.runnerUsable ?? record.ok ?? false,
|
||||
contractOk: record.contractOk ?? record.ok ?? false,
|
||||
path: record.path ?? null,
|
||||
resolvedPath: record.resolvedPath ?? record.path ?? null,
|
||||
resolvedPathSource: record.resolvedPathSource ?? null,
|
||||
resolution: record.resolution ?? null,
|
||||
source: record.source ?? null,
|
||||
target: record.target ?? null,
|
||||
mountPoint: record.mountPoint ?? null,
|
||||
@@ -6086,8 +6091,13 @@ function compactSkillsStatus(value: unknown): Record<string, unknown> | null {
|
||||
available: record.available ?? false,
|
||||
degraded: record.degraded ?? true,
|
||||
blocker: record.blocker ?? null,
|
||||
degradedReason: record.degradedReason ?? record.blocker ?? null,
|
||||
readonly: record.readonly ?? false,
|
||||
skillCount: record.skillCount ?? 0,
|
||||
sourceSkillCount: record.sourceSkillCount ?? null,
|
||||
targetSkillCount: record.targetSkillCount ?? null,
|
||||
sourceMissingSkills: Array.isArray(record.sourceMissingSkills) ? record.sourceMissingSkills.map(String) : [],
|
||||
targetMissingSkills: Array.isArray(record.targetMissingSkills) ? record.targetMissingSkills.map(String) : [],
|
||||
requiredSkills: Array.isArray(record.requiredSkills) ? record.requiredSkills : [],
|
||||
missingSkills: Array.isArray(record.missingSkills) ? record.missingSkills : [],
|
||||
valuesPrinted: record.valuesPrinted ?? false,
|
||||
@@ -6115,6 +6125,8 @@ function compactSkillPathReport(value: unknown): Record<string, unknown> | null
|
||||
writable: record.writable ?? false,
|
||||
readonly: record.readonly ?? false,
|
||||
mountPoint: record.mountPoint ?? null,
|
||||
symlink: record.symlink ?? false,
|
||||
realPath: record.realPath ?? null,
|
||||
skillCount: record.skillCount ?? 0,
|
||||
requiredSkills: Array.isArray(record.requiredSkills) ? record.requiredSkills.map(String) : [],
|
||||
missingSkills: Array.isArray(record.missingSkills) ? record.missingSkills.map(String) : [],
|
||||
@@ -6729,6 +6741,7 @@ function compactPrPreflightCommanderView(record: Record<string, unknown>, option
|
||||
capabilitySource: authBrokerCapability?.source ?? tokenCoverage?.credentialSource ?? null,
|
||||
},
|
||||
},
|
||||
skillsContract: record.skillsContract ?? undefined,
|
||||
activeRunnerPrCapability: {
|
||||
scope: activeRunnerDevContainer.scope ?? "current-cli-process",
|
||||
tokenCandidatePresent: activeRunnerTokenCandidatePresent,
|
||||
@@ -6902,11 +6915,11 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
const risks = Array.isArray(pull.risks) ? pull.risks.map(String) : [];
|
||||
const ok = preflight.ok === true && tokenCoverage.ok === true;
|
||||
const githubTransient = githubTransientEvidence(pull);
|
||||
const skillsBlocked = skills !== null && skills.ok !== true;
|
||||
const skillsSyncBlocked = skillsSync !== null && skillsSync.ok !== true;
|
||||
const skillsBlocked = skills !== null && skills.runnerUsable !== true && skills.ok !== true;
|
||||
const skillsContractDegraded = skills !== null && skills.contractOk !== true;
|
||||
const failureKind = !tokenCoverage.ok
|
||||
? "auth-missing"
|
||||
: skillsBlocked || skillsSyncBlocked
|
||||
: skillsBlocked
|
||||
? "runner-skills-blocker"
|
||||
: githubTransient !== null
|
||||
? "github-transient"
|
||||
@@ -6918,7 +6931,7 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
const degradedReason = failureKind === "auth-missing"
|
||||
? "auth-broker-needed"
|
||||
: failureKind === "runner-skills-blocker"
|
||||
? typeof skillsSync?.blocker === "string" ? skillsSync.blocker : typeof skills?.blocker === "string" ? skills.blocker : "runner-skills-degraded"
|
||||
? typeof skills?.degradedReason === "string" ? skills.degradedReason : typeof skills?.blocker === "string" ? skills.blocker : typeof skillsSync?.blocker === "string" ? skillsSync.blocker : "runner-skills-degraded"
|
||||
: failureKind === "github-transient"
|
||||
? "github-dns-api-transient"
|
||||
: failureKind === "git-remote-gap"
|
||||
@@ -6948,6 +6961,13 @@ function compactPrRuntimePreflight(preflight: Record<string, unknown>, options:
|
||||
},
|
||||
skills,
|
||||
skillsSync,
|
||||
skillsContract: {
|
||||
ok: !skillsContractDegraded,
|
||||
degraded: skillsContractDegraded,
|
||||
hostRolloutRequired: asRecord(skills?.resolution)?.hostRolloutRequired === true,
|
||||
degradedReason: skills?.degradedReason ?? skills?.blocker ?? skillsSync?.blocker ?? null,
|
||||
note: skillsContractDegraded ? "runner skills are usable through the resolved path, but the read-only target projection still needs host rollout repair" : null,
|
||||
},
|
||||
tokenCoverage,
|
||||
authBroker: authBrokerNeededStatus(tokenCoverage, authBrokerRuntime, systemGhBinary, unideskGhCli),
|
||||
authScopeSummary,
|
||||
@@ -7319,6 +7339,7 @@ function codeQueuePrPreflight(optionArgs: string[] = [], transport: CodeQueuePrP
|
||||
scopeBoundary: compact.scopeBoundary,
|
||||
activeRunnerDevContainer: compact.activeRunnerDevContainer,
|
||||
recommendedActions: compact.recommendedActions,
|
||||
skillsContract: compact.skillsContract,
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
controlPlane: {
|
||||
mode: "local-backend-core",
|
||||
@@ -7378,6 +7399,7 @@ export async function codexPrPreflightQueryAsync(optionArgs: string[], fetcher:
|
||||
scopeBoundary: compact.scopeBoundary,
|
||||
activeRunnerDevContainer: compact.activeRunnerDevContainer,
|
||||
recommendedActions: compact.recommendedActions,
|
||||
skillsContract: compact.skillsContract,
|
||||
upstream: { ok: response.ok, status: response.status },
|
||||
controlPlane: {
|
||||
mode: "remote-frontend",
|
||||
|
||||
@@ -626,7 +626,12 @@ function compactSkillAvailability(value: unknown): Record<string, unknown> | nul
|
||||
if (skills === null) return null;
|
||||
return {
|
||||
ok: skills.ok ?? false,
|
||||
runnerUsable: skills.runnerUsable ?? skills.ok ?? false,
|
||||
contractOk: skills.contractOk ?? skills.ok ?? false,
|
||||
path: skills.path ?? null,
|
||||
resolvedPath: skills.resolvedPath ?? skills.path ?? null,
|
||||
resolvedPathSource: skills.resolvedPathSource ?? null,
|
||||
resolution: skills.resolution ?? null,
|
||||
source: skills.source ?? null,
|
||||
target: skills.target ?? null,
|
||||
mountPoint: skills.mountPoint ?? null,
|
||||
@@ -634,8 +639,13 @@ function compactSkillAvailability(value: unknown): Record<string, unknown> | nul
|
||||
available: skills.available ?? false,
|
||||
degraded: skills.degraded ?? true,
|
||||
blocker: skills.blocker ?? null,
|
||||
degradedReason: skills.degradedReason ?? skills.blocker ?? null,
|
||||
readonly: skills.readonly ?? false,
|
||||
skillCount: skills.skillCount ?? 0,
|
||||
sourceSkillCount: skills.sourceSkillCount ?? null,
|
||||
targetSkillCount: skills.targetSkillCount ?? null,
|
||||
sourceMissingSkills: Array.isArray(skills.sourceMissingSkills) ? skills.sourceMissingSkills.map(String) : [],
|
||||
targetMissingSkills: Array.isArray(skills.targetMissingSkills) ? skills.targetMissingSkills.map(String) : [],
|
||||
requiredSkills: Array.isArray(skills.requiredSkills) ? skills.requiredSkills.map(String) : [],
|
||||
missingSkills: Array.isArray(skills.missingSkills) ? skills.missingSkills.map(String) : [],
|
||||
valuesPrinted: skills.valuesPrinted ?? false,
|
||||
@@ -663,6 +673,8 @@ function compactSkillSync(value: unknown): Record<string, unknown> | null {
|
||||
"writable",
|
||||
"readonly",
|
||||
"mountPoint",
|
||||
"symlink",
|
||||
"realPath",
|
||||
"skillCount",
|
||||
"requiredSkills",
|
||||
"missingSkills",
|
||||
@@ -677,6 +689,8 @@ function compactSkillSync(value: unknown): Record<string, unknown> | null {
|
||||
"writable",
|
||||
"readonly",
|
||||
"mountPoint",
|
||||
"symlink",
|
||||
"realPath",
|
||||
"skillCount",
|
||||
"requiredSkills",
|
||||
"missingSkills",
|
||||
|
||||
Reference in New Issue
Block a user