feat: 为 gitbundle 装配 required skills (#138)

* Add gitbundle required skills validation

* fix: 限定 required skill blocked result 覆盖

---------

Co-authored-by: AgentRun Codex <agentrun@example.invalid>
Co-authored-by: Codex <codex@pikas.tech>
This commit is contained in:
Lyon
2026-06-10 10:36:26 +08:00
committed by GitHub
parent 2e95276db8
commit 74b83b43c2
12 changed files with 247 additions and 45 deletions
+40 -6
View File
@@ -11,7 +11,7 @@ import type { JsonRecord, ResourceBundleRef } from "../../common/types.js";
import { assertNoSecretLeak, type SelfTestCase, type SelfTestContext } from "../harness.js";
const execFile = promisify(execFileCallback);
type LocalBundle = { repoUrl: string; commitId: string; branch?: string; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"] };
type LocalBundle = { repoUrl: string; commitId: string; branch?: string; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"]; requiredSkills?: ResourceBundleRef["requiredSkills"] };
type RefResolvedBundle = LocalBundle & { branch: string; latestCommitId: string };
const selfTest: SelfTestCase = async (context) => {
@@ -93,12 +93,19 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.equal(((resultEnvelope.sessionRef as JsonRecord).threadId), "thread_selftest_1");
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).commitId), bundle.commitId);
assert.equal(((resultEnvelope.resourceBundleRef as JsonRecord).kind), "gitbundle");
assert.deepEqual((((resultEnvelope.resourceBundleRef as JsonRecord).requiredSkills as JsonRecord).names), ["dad-dev"]);
const resultBundleTargets = (((resultEnvelope.resourceBundleRef as JsonRecord).bundles as JsonRecord).items as JsonRecord[]).map((item) => item.targetPath);
assert.deepEqual(resultBundleTargets, ["tools", ".agents/skills"]);
const materialized = ((resultEnvelope.resourceBundleRef as JsonRecord).materialized as JsonRecord);
assert.deepEqual(((materialized.tools as JsonRecord).names), ["hwpod"]);
assert.equal(((materialized.tools as JsonRecord).installed), true);
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]);
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
const requiredSkillItems = ((materialized.requiredSkills as JsonRecord).items as JsonRecord[]);
assert.deepEqual(((materialized.requiredSkills as JsonRecord).names), ["dad-dev"]);
assert.equal(requiredSkillItems[0]?.status, "materialized");
assert.equal(typeof requiredSkillItems[0]?.manifestSha256, "string");
assert.equal(typeof requiredSkillItems[0]?.manifestBytes, "number");
assert.equal(((requiredSkillItems[0]?.sourceBundle as JsonRecord).commitId), bundle.commitId);
assertNoSecretLeak(resultEnvelope);
const refBundle = await createRefResolvedLocalGitBundle(context);
@@ -129,6 +136,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
const firstAssemblyInput = turnInputText(assemblyInputs[0]);
const secondAssemblyInput = turnInputText(assemblyInputs[1]);
assert.match(firstAssemblyInput, /HWLAB v0\.2 runtime prompt self-test/u);
assert.match(firstAssemblyInput, /dad-dev/u);
assert.match(firstAssemblyInput, /hwpod-cli/u);
assert.match(firstAssemblyInput, /hwpod-ctl/u);
assert.match(firstAssemblyInput, /hwpod-compiler-cli/u);
@@ -149,7 +157,8 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.deepEqual(assemblyBundleTargets, ["tools", ".agents/skills"]);
const assemblyMaterialized = assemblyResource.materialized as JsonRecord;
assert.deepEqual(((assemblyMaterialized.promptRefs as JsonRecord).names), ["hwlab-v02-runtime"]);
assert.deepEqual(((assemblyMaterialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]);
assert.deepEqual(((assemblyMaterialized.skillDirs as JsonRecord).names), ["dad-dev", "hwpod-cli", "hwpod-ctl"]);
assert.deepEqual(((assemblyMaterialized.requiredSkills as JsonRecord).names), ["dad-dev"]);
assert.equal(((assemblyMaterialized.initialPrompt as JsonRecord).available), true);
assertNoSecretLeak(assemblyEnvelope);
@@ -158,6 +167,21 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
assert.equal(missingPromptResult.terminalStatus, "blocked");
assert.equal(missingPromptResult.failureKind, "prompt-unavailable");
const missingSkillRun = await createHwlabRun(client, context, { ...bundle, bundles: [{ name: "hwlab-tools", subpath: "tools", targetPath: "tools" }], requiredSkills: [{ name: "dad-dev" }] }, "hwlab-session-missing-skill", "missing required skill", "hwlab-command-missing-skill");
const missingSkillResult = await runOnce({ managerUrl: server.baseUrl, runId: missingSkillRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-missing-skill") }, oneShot: true }) as JsonRecord;
assert.equal(missingSkillResult.terminalStatus, "blocked");
assert.equal(missingSkillResult.failureKind, "required-skill-unavailable");
const missingSkillEnvelope = await client.get(`/api/v1/runs/${missingSkillRun.runId}/commands/${missingSkillRun.commandId}/result`) as JsonRecord;
assert.equal(missingSkillEnvelope.terminalStatus, "blocked");
assert.equal(missingSkillEnvelope.terminalSource, "terminal_status-event");
assert.equal(missingSkillEnvelope.failureKind, "required-skill-unavailable");
assert.deepEqual((((missingSkillEnvelope.resourceBundleRef as JsonRecord).requiredSkills as JsonRecord).names), ["dad-dev"]);
const missingSkillBlocker = missingSkillEnvelope.blocker as JsonRecord;
assert.equal(missingSkillBlocker.failureKind, "required-skill-unavailable");
assert.deepEqual(((missingSkillBlocker.details as JsonRecord).required as string[]), ["dad-dev"]);
assert.deepEqual((((missingSkillBlocker.details as JsonRecord).missing as JsonRecord[]).map((item) => item.name)), ["dad-dev"]);
assertNoSecretLeak(missingSkillEnvelope);
const resumed = await createHwlabRun(client, context, bundle, "hwlab-session-resume", "hello resumed", "hwlab-command-session-resumed");
const resumedRun = await client.get(`/api/v1/runs/${resumed.runId}`) as JsonRecord;
assert.equal(((resumedRun.sessionRef as JsonRecord).threadId), "thread_selftest_1");
@@ -237,7 +261,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
const runningResult = await running;
assert.equal(runningResult.terminalStatus, "cancelled");
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-gitbundle-materialization", "gitbundle-ref-resolution", "gitbundle-tools-path", "gitbundle-skill-dir-assembly", "resource-prompt-required-blocker", "same-run-runner-multiturn", "running-steer", "idle-after-tool-liveness", "running-cancel"] };
return { name: "hwlab-manual-dispatch", tests: ["runner-job-idempotency", "pending-cancel", "result-envelope", "session-ref-resume", "resource-gitbundle-materialization", "gitbundle-ref-resolution", "gitbundle-tools-path", "gitbundle-skill-dir-assembly", "resource-prompt-required-blocker", "resource-required-skill-blocker", "same-run-runner-multiturn", "running-steer", "idle-after-tool-liveness", "running-cancel"] };
} finally {
await new Promise<void>((resolve) => server.server.close(() => resolve()));
}
@@ -262,6 +286,15 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
"Compile D601-F103-V2 through hwpod-cli -> hwpod-compiler-cli -> /v1/hwpod-node-ops -> hwpod-node.",
"Do not invent fallback hardware paths or legacy profile routes.",
].join("\n"), "utf8");
await mkdir(path.join(repo, "skills", "dad-dev"), { recursive: true });
await writeFile(path.join(repo, "skills", "dad-dev", "SKILL.md"), [
"---",
"name: dad-dev",
"description: Track dad-dev issue progress and validate delivery evidence for AgentRun work.",
"---",
"# dad-dev",
"Record P1-P4 evidence and remaining risk for AgentRun issue work.",
].join("\n"), "utf8");
await mkdir(path.join(repo, "skills", "hwpod-cli", "scripts"), { recursive: true });
await writeFile(path.join(repo, "skills", "hwpod-cli", "SKILL.md"), [
"---",
@@ -282,10 +315,10 @@ async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle
"Use hwpod-ctl for HWPOD runtime inspection and control-plane state.",
].join("\n"), "utf8");
await writeFile(path.join(repo, "skills", "hwpod-ctl", "scripts", "hwpod-ctl.mjs"), "console.log(JSON.stringify({ ok: true, cli: 'hwpod-ctl-skill-selftest' }));\n", "utf8");
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
await execFile("git", ["add", "README.md", "tools/hwpod", "tools/hwpod-cli.ts", "tools/src/hwpod-harness-lib.ts", "tools/hwpod-node.test.ts", "internal/agent/prompts/hwlab-v02-runtime.md", "skills/dad-dev/SKILL.md", "skills/hwpod-cli/SKILL.md", "skills/hwpod-cli/scripts/hwpod-cli.mjs", "skills/hwpod-ctl/SKILL.md", "skills/hwpod-ctl/scripts/hwpod-ctl.mjs"], { cwd: repo });
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest"], { cwd: repo });
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: repo });
return { repoUrl: repo, commitId: stdout.trim() };
return { repoUrl: repo, commitId: stdout.trim(), requiredSkills: [{ name: "dad-dev" }] };
}
async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promise<RefResolvedBundle> {
@@ -304,6 +337,7 @@ async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promis
async function createHwlabRun(client: ManagerClient, context: SelfTestContext, bundle: LocalBundle, sessionId: string, prompt: string, idempotencyKey: string, timeoutMs = 15_000): Promise<{ runId: string; commandId: string }> {
const resourceBundleRef: ResourceBundleRef = { kind: "gitbundle", repoUrl: bundle.repoUrl, commitId: bundle.commitId, bundles: bundle.bundles ?? defaultGitBundles(), submodules: false, lfs: false };
if (bundle.promptRefs) resourceBundleRef.promptRefs = bundle.promptRefs;
if (bundle.requiredSkills) resourceBundleRef.requiredSkills = bundle.requiredSkills;
const run = await client.post("/api/v1/runs", {
tenantId: "hwlab",
projectId: "pikasTech/HWLAB",