fix: resolve gitbundle source from repo ref
This commit is contained in:
+3
-1
@@ -60,6 +60,7 @@ export interface GitBundleItemRef extends JsonRecord {
|
||||
name?: string;
|
||||
repoUrl?: string;
|
||||
commitId?: string;
|
||||
ref?: string;
|
||||
subpath: string;
|
||||
targetPath: string;
|
||||
}
|
||||
@@ -67,7 +68,8 @@ export interface GitBundleItemRef extends JsonRecord {
|
||||
export interface ResourceBundleRef extends JsonRecord {
|
||||
kind: "gitbundle";
|
||||
repoUrl: string;
|
||||
commitId: string;
|
||||
commitId?: string;
|
||||
ref?: string;
|
||||
bundles: GitBundleItemRef[];
|
||||
promptRefs?: Array<{
|
||||
name: string;
|
||||
|
||||
@@ -85,10 +85,11 @@ export function validateResourceBundleRef(value: unknown): ResourceBundleRef | n
|
||||
const kind = requiredString(record, "kind");
|
||||
if (kind !== "gitbundle") throw new AgentRunError("schema-invalid", "resourceBundleRef.kind must be gitbundle", { httpStatus: 400 });
|
||||
const repoUrl = requiredString(record, "repoUrl");
|
||||
const commitId = requiredString(record, "commitId");
|
||||
validateCommitId(commitId, "resourceBundleRef.commitId");
|
||||
const commitId = optionalString(record.commitId);
|
||||
if (commitId) validateCommitId(commitId, "resourceBundleRef.commitId");
|
||||
const ref = validateGitRef(record.ref, "resourceBundleRef.ref");
|
||||
rejectLegacyResourceBundleFields(record);
|
||||
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, commitId, bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId) };
|
||||
const result: ResourceBundleRef = { kind: "gitbundle", repoUrl, ...(commitId ? { commitId } : {}), ...(ref ? { ref } : {}), bundles: validateResourceGitBundles(record.bundles, repoUrl, commitId, ref) };
|
||||
if (record.promptRefs !== undefined) result.promptRefs = validateResourcePromptRefs(record.promptRefs);
|
||||
if (record.submodules !== undefined && record.submodules !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.submodules can only be false in v0.1", { httpStatus: 400 });
|
||||
if (record.lfs !== undefined && record.lfs !== false) throw new AgentRunError("schema-invalid", "resourceBundleRef.lfs can only be false in v0.1", { httpStatus: 400 });
|
||||
@@ -104,7 +105,7 @@ function rejectLegacyResourceBundleFields(record: JsonRecord): void {
|
||||
}
|
||||
}
|
||||
|
||||
function validateResourceGitBundles(value: unknown, defaultRepoUrl: string, defaultCommitId: string): ResourceBundleRef["bundles"] {
|
||||
function validateResourceGitBundles(value: unknown, defaultRepoUrl: string, defaultCommitId?: string, defaultRef?: string): ResourceBundleRef["bundles"] {
|
||||
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must be an array", { httpStatus: 400 });
|
||||
if (value.length === 0) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must contain at least one entry", { httpStatus: 400 });
|
||||
if (value.length > 64) throw new AgentRunError("schema-invalid", "resourceBundleRef.bundles must contain at most 64 entries", { httpStatus: 400 });
|
||||
@@ -115,15 +116,16 @@ function validateResourceGitBundles(value: unknown, defaultRepoUrl: string, defa
|
||||
if (name) validateResourceName(name, `resourceBundleRef.bundles[${index}].name`);
|
||||
const repoUrl = optionalString(record.repoUrl) ?? defaultRepoUrl;
|
||||
const commitId = optionalString(record.commitId) ?? defaultCommitId;
|
||||
validateCommitId(commitId, `resourceBundleRef.bundles[${index}].commitId`);
|
||||
if (commitId) validateCommitId(commitId, `resourceBundleRef.bundles[${index}].commitId`);
|
||||
const ref = validateGitRef(record.ref, `resourceBundleRef.bundles[${index}].ref`) ?? (commitId ? undefined : defaultRef);
|
||||
const subpath = validateBundleSubpath(requiredString(record, "subpath"), `resourceBundleRef.bundles[${index}].subpath`);
|
||||
const rawTargetPath = typeof record.targetPath === "string" ? record.targetPath : record.target_path;
|
||||
if (typeof rawTargetPath !== "string" || rawTargetPath.trim().length === 0) throw new AgentRunError("schema-invalid", `resourceBundleRef.bundles[${index}].target_path is required`, { httpStatus: 400 });
|
||||
const targetPath = validateWorkspaceRelativePath(rawTargetPath.trim(), `resourceBundleRef.bundles[${index}].target_path`);
|
||||
const identity = `${repoUrl}\0${commitId}\0${subpath}\0${targetPath}`;
|
||||
const identity = `${repoUrl}\0${commitId ?? ""}\0${ref ?? ""}\0${subpath}\0${targetPath}`;
|
||||
if (seen.has(identity)) throw new AgentRunError("schema-invalid", `resourceBundleRef.bundles[${index}] duplicates an earlier bundle target`, { httpStatus: 400 });
|
||||
seen.add(identity);
|
||||
return { ...(name ? { name } : {}), ...(repoUrl === defaultRepoUrl ? {} : { repoUrl }), ...(commitId === defaultCommitId ? {} : { commitId }), subpath, targetPath };
|
||||
return { ...(name ? { name } : {}), ...(repoUrl === defaultRepoUrl ? {} : { repoUrl }), ...(commitId && commitId !== defaultCommitId ? { commitId } : {}), ...(ref && ref !== defaultRef ? { ref } : {}), subpath, targetPath };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,6 +133,15 @@ function validateCommitId(commitId: string, fieldName: string): void {
|
||||
if (!/^[0-9a-f]{40}$/u.test(commitId)) throw new AgentRunError("schema-invalid", `${fieldName} must be a full 40-character git commit sha`, { httpStatus: 400 });
|
||||
}
|
||||
|
||||
function validateGitRef(value: unknown, fieldName: string): string | undefined {
|
||||
const ref = optionalString(value);
|
||||
if (!ref) return undefined;
|
||||
if (ref.length > 200 || ref.startsWith("-") || ref.includes("..") || ref.includes("@{") || ref.endsWith("/") || ref.endsWith(".") || /[\s~^:?*[\\\x00-\x1f\x7f]/u.test(ref)) {
|
||||
throw new AgentRunError("schema-invalid", `${fieldName} must be a bounded git ref name`, { httpStatus: 400 });
|
||||
}
|
||||
return ref;
|
||||
}
|
||||
|
||||
function validateResourcePromptRefs(value: unknown): NonNullable<ResourceBundleRef["promptRefs"]> {
|
||||
if (!Array.isArray(value)) throw new AgentRunError("schema-invalid", "resourceBundleRef.promptRefs must be an array", { httpStatus: 400 });
|
||||
if (value.length > 16) throw new AgentRunError("schema-invalid", "resourceBundleRef.promptRefs must contain at most 16 entries", { httpStatus: 400 });
|
||||
|
||||
+3
-2
@@ -171,10 +171,11 @@ function resourceBundleSummary(run: RunRecord, events: RunEvent[]): JsonRecord |
|
||||
return {
|
||||
kind: run.resourceBundleRef.kind,
|
||||
repoUrl: run.resourceBundleRef.repoUrl,
|
||||
commitId: run.resourceBundleRef.commitId,
|
||||
commitId: run.resourceBundleRef.commitId ?? null,
|
||||
ref: run.resourceBundleRef.ref ?? null,
|
||||
bundles: {
|
||||
count: run.resourceBundleRef.bundles.length,
|
||||
items: run.resourceBundleRef.bundles.map((item) => ({ name: item.name ?? null, repoUrl: item.repoUrl ?? run.resourceBundleRef?.repoUrl ?? null, commitId: item.commitId ?? run.resourceBundleRef?.commitId ?? null, subpath: item.subpath, targetPath: item.targetPath, valuesPrinted: false })),
|
||||
items: run.resourceBundleRef.bundles.map((item) => ({ name: item.name ?? null, repoUrl: item.repoUrl ?? run.resourceBundleRef?.repoUrl ?? null, commitId: item.commitId ?? run.resourceBundleRef?.commitId ?? null, ref: item.ref ?? run.resourceBundleRef?.ref ?? null, subpath: item.subpath, targetPath: item.targetPath, valuesPrinted: false })),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
promptRefs: run.resourceBundleRef.promptRefs ? { count: run.resourceBundleRef.promptRefs.length, names: run.resourceBundleRef.promptRefs.map((item) => item.name), required: run.resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
||||
|
||||
+3
-2
@@ -731,10 +731,11 @@ export function summarizeResourceBundleRef(resourceBundleRef: RunRecord["resourc
|
||||
return {
|
||||
kind: resourceBundleRef.kind,
|
||||
repoUrl: resourceBundleRef.repoUrl,
|
||||
commitId: resourceBundleRef.commitId,
|
||||
commitId: resourceBundleRef.commitId ?? null,
|
||||
ref: resourceBundleRef.ref ?? null,
|
||||
bundles: {
|
||||
count: resourceBundleRef.bundles.length,
|
||||
items: resourceBundleRef.bundles.map((item) => ({ name: item.name ?? null, repoUrl: item.repoUrl ?? resourceBundleRef.repoUrl, commitId: item.commitId ?? resourceBundleRef.commitId, subpath: item.subpath, targetPath: item.targetPath, valuesPrinted: false })),
|
||||
items: resourceBundleRef.bundles.map((item) => ({ name: item.name ?? null, repoUrl: item.repoUrl ?? resourceBundleRef.repoUrl, commitId: item.commitId ?? resourceBundleRef.commitId ?? null, ref: item.ref ?? resourceBundleRef.ref ?? null, subpath: item.subpath, targetPath: item.targetPath, valuesPrinted: false })),
|
||||
valuesPrinted: false,
|
||||
},
|
||||
promptRefs: resourceBundleRef.promptRefs ? { count: resourceBundleRef.promptRefs.length, names: resourceBundleRef.promptRefs.map((item) => item.name), required: resourceBundleRef.promptRefs.filter((item) => item.required === true).map((item) => item.name), valuesPrinted: false } : { count: 0, names: [], required: [], valuesPrinted: false },
|
||||
|
||||
@@ -43,14 +43,24 @@ interface MaterializedSkillRef {
|
||||
interface GitCheckout {
|
||||
repoUrl: string;
|
||||
commitId: string;
|
||||
requestedCommitId?: string;
|
||||
requestedRef?: string;
|
||||
checkoutPath: string;
|
||||
treeId: string;
|
||||
}
|
||||
|
||||
interface GitBundleSource {
|
||||
repoUrl: string;
|
||||
commitId?: string;
|
||||
ref?: string;
|
||||
}
|
||||
|
||||
interface MaterializedGitBundle {
|
||||
name: string | null;
|
||||
repoUrl: string;
|
||||
commitId: string;
|
||||
requestedCommitId: string | null;
|
||||
requestedRef: string | null;
|
||||
subpath: string;
|
||||
targetPath: string;
|
||||
sourceKind: "file" | "directory";
|
||||
@@ -67,18 +77,19 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
await rm(assemblyRoot, { recursive: true, force: true });
|
||||
await mkdir(checkoutRoot, { recursive: true });
|
||||
await mkdir(workspacePath, { recursive: true });
|
||||
const defaultSource = defaultGitBundleSource(resourceBundleRef, env);
|
||||
const checkoutCache = new Map<string, Promise<GitCheckout>>();
|
||||
const checkoutFor = (repoUrl: string, commitId: string) => {
|
||||
const key = stableHash({ repoUrl, commitId });
|
||||
const checkoutFor = (source: GitBundleSource) => {
|
||||
const key = stableHash(gitSourceIdentity(source));
|
||||
let checkout = checkoutCache.get(key);
|
||||
if (!checkout) {
|
||||
checkout = checkoutGitCommit(checkoutRoot, repoUrl, commitId);
|
||||
checkout = checkoutGitSource(checkoutRoot, source);
|
||||
checkoutCache.set(key, checkout);
|
||||
}
|
||||
return checkout;
|
||||
};
|
||||
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, checkoutFor);
|
||||
const defaultCheckout = await checkoutFor(resourceBundleRef.repoUrl, resourceBundleRef.commitId);
|
||||
const defaultCheckout = await checkoutFor(defaultSource);
|
||||
const materializedBundles = await materializeGitBundles(workspacePath, resourceBundleRef, defaultSource, defaultCheckout, checkoutFor);
|
||||
const tools = await prepareGitBundleTools(workspacePath);
|
||||
const skills = await discoverGitBundleSkills(workspacePath);
|
||||
const prompts = await materializePromptRefs(defaultCheckout.checkoutPath, resourceBundleRef.promptRefs ?? []);
|
||||
@@ -92,7 +103,9 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
phase: "resource-bundle-materialized",
|
||||
kind: "gitbundle",
|
||||
repoUrl: resourceBundleRef.repoUrl,
|
||||
commitId: resourceBundleRef.commitId,
|
||||
commitId: defaultCheckout.commitId,
|
||||
requestedCommitId: resourceBundleRef.commitId ?? null,
|
||||
requestedRef: defaultCheckout.requestedRef ?? null,
|
||||
treeId: defaultCheckout.treeId,
|
||||
checkoutPath: pathSummary(defaultCheckout.checkoutPath),
|
||||
workspacePath: pathSummary(workspacePath),
|
||||
@@ -110,26 +123,56 @@ export async function materializeResourceBundle(resourceBundleRef: ResourceBundl
|
||||
};
|
||||
}
|
||||
|
||||
async function checkoutGitCommit(checkoutRoot: string, repoUrl: string, commitId: string): Promise<GitCheckout> {
|
||||
const checkoutPath = path.join(checkoutRoot, stableHash({ repoUrl, commitId }).slice(0, 16));
|
||||
function defaultGitBundleSource(resourceBundleRef: ResourceBundleRef, env: NodeJS.ProcessEnv): GitBundleSource {
|
||||
const ref = optionalNonEmpty(resourceBundleRef.ref) ?? optionalNonEmpty(env.AGENTRUN_RESOURCE_BUNDLE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_REF) ?? optionalNonEmpty(env.AGENTRUN_WORKSPACE_BRANCH);
|
||||
if (ref) return { repoUrl: resourceBundleRef.repoUrl, ref };
|
||||
const commitId = optionalNonEmpty(resourceBundleRef.commitId);
|
||||
if (commitId) return { repoUrl: resourceBundleRef.repoUrl, commitId };
|
||||
return { repoUrl: resourceBundleRef.repoUrl, ref: "HEAD" };
|
||||
}
|
||||
|
||||
function bundleGitSource(bundle: ResourceBundleRef["bundles"][number], resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource): GitBundleSource {
|
||||
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
|
||||
const ref = optionalNonEmpty(bundle.ref);
|
||||
if (ref) return { repoUrl, ref };
|
||||
const commitId = optionalNonEmpty(bundle.commitId);
|
||||
if (commitId) return { repoUrl, commitId };
|
||||
if (repoUrl === defaultSource.repoUrl) return defaultSource;
|
||||
if (defaultSource.ref) return { repoUrl, ref: defaultSource.ref };
|
||||
if (defaultSource.commitId) return { repoUrl, commitId: defaultSource.commitId };
|
||||
return { repoUrl, ref: "HEAD" };
|
||||
}
|
||||
|
||||
async function checkoutGitSource(checkoutRoot: string, source: GitBundleSource): Promise<GitCheckout> {
|
||||
const checkoutPath = path.join(checkoutRoot, stableHash(gitSourceIdentity(source)).slice(0, 16));
|
||||
await mkdir(checkoutPath, { recursive: true });
|
||||
await git(["init"], checkoutPath);
|
||||
await git(["remote", "remove", "origin"], checkoutPath, { allowFailure: true });
|
||||
await git(["remote", "add", "origin", repoUrl], checkoutPath);
|
||||
await git(["fetch", "--depth", "1", "origin", commitId], checkoutPath);
|
||||
await git(["checkout", "--detach", commitId], checkoutPath);
|
||||
await git(["remote", "add", "origin", source.repoUrl], checkoutPath);
|
||||
if (source.ref) {
|
||||
await git(["fetch", "--depth", "1", "origin", source.ref], checkoutPath);
|
||||
await git(["checkout", "--detach", "FETCH_HEAD"], checkoutPath);
|
||||
} else if (source.commitId) {
|
||||
await git(["fetch", "--depth", "1", "origin", source.commitId], checkoutPath);
|
||||
await git(["checkout", "--detach", source.commitId], checkoutPath);
|
||||
} else {
|
||||
throw new AgentRunError("schema-invalid", "gitbundle source must include repo ref or commit", { httpStatus: 400 });
|
||||
}
|
||||
const actualCommit = (await git(["rev-parse", "HEAD"], checkoutPath)).stdout.trim();
|
||||
if (actualCommit !== commitId) throw new AgentRunError("infra-failed", "gitbundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: commitId, actualCommit } });
|
||||
if (source.commitId && actualCommit !== source.commitId) throw new AgentRunError("infra-failed", "gitbundle checkout did not land on requested commit", { httpStatus: 500, details: { expectedCommit: source.commitId, actualCommit } });
|
||||
const treeId = (await git(["rev-parse", "HEAD^{tree}"], checkoutPath)).stdout.trim();
|
||||
return { repoUrl, commitId, checkoutPath, treeId };
|
||||
return { repoUrl: source.repoUrl, commitId: actualCommit, ...(source.commitId ? { requestedCommitId: source.commitId } : {}), ...(source.ref ? { requestedRef: source.ref } : {}), checkoutPath, treeId };
|
||||
}
|
||||
|
||||
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, checkoutFor: (repoUrl: string, commitId: string) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
|
||||
function gitSourceIdentity(source: GitBundleSource): JsonRecord {
|
||||
return { repoUrl: source.repoUrl, commitId: source.commitId ?? null, ref: source.ref ?? null };
|
||||
}
|
||||
|
||||
async function materializeGitBundles(workspacePath: string, resourceBundleRef: ResourceBundleRef, defaultSource: GitBundleSource, defaultCheckout: GitCheckout, checkoutFor: (source: GitBundleSource) => Promise<GitCheckout>): Promise<MaterializedGitBundle[]> {
|
||||
const items: MaterializedGitBundle[] = [];
|
||||
for (const [index, bundle] of resourceBundleRef.bundles.entries()) {
|
||||
const repoUrl = bundle.repoUrl ?? resourceBundleRef.repoUrl;
|
||||
const commitId = bundle.commitId ?? resourceBundleRef.commitId;
|
||||
const checkout = await checkoutFor(repoUrl, commitId);
|
||||
const gitSource = bundleGitSource(bundle, resourceBundleRef, defaultSource);
|
||||
const checkout = gitSource === defaultSource ? defaultCheckout : await checkoutFor(gitSource);
|
||||
const source = resolveBundlePath(checkout.checkoutPath, bundle.subpath, `bundles[${index}].subpath`);
|
||||
const target = resolveWorkspaceTargetPath(workspacePath, bundle.targetPath, `bundles[${index}].target_path`);
|
||||
let sourceStat;
|
||||
@@ -141,11 +184,15 @@ async function materializeGitBundles(workspacePath: string, resourceBundleRef: R
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
await rm(target, { recursive: true, force: true });
|
||||
await cp(source, target, { recursive: true, force: true, dereference: false });
|
||||
items.push({ name: bundle.name ?? null, repoUrl, commitId, subpath: bundle.subpath, targetPath: bundle.targetPath, sourceKind: sourceStat.isDirectory() ? "directory" : "file", sourceBytes: sourceStat.isFile() ? sourceStat.size : null });
|
||||
items.push({ name: bundle.name ?? null, repoUrl: checkout.repoUrl, commitId: checkout.commitId, requestedCommitId: bundle.commitId ?? resourceBundleRef.commitId ?? null, requestedRef: checkout.requestedRef ?? null, subpath: bundle.subpath, targetPath: bundle.targetPath, sourceKind: sourceStat.isDirectory() ? "directory" : "file", sourceBytes: sourceStat.isFile() ? sourceStat.size : null });
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function optionalNonEmpty(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
async function prepareGitBundleTools(workspacePath: string): Promise<{ binPath?: string; event: JsonRecord }> {
|
||||
const binPath = path.join(workspacePath, "tools");
|
||||
let entries;
|
||||
|
||||
@@ -95,7 +95,7 @@ export async function runOnce(options: RunnerOnceOptions): Promise<JsonRecord> {
|
||||
if (!materializationAttempted) {
|
||||
materializationAttempted = true;
|
||||
try {
|
||||
const materialized = await materializeResourceBundle(claimed.resourceBundleRef ?? null, resourceMaterializationEnv(options.env ?? process.env, options.runId, attemptId));
|
||||
const materialized = await materializeResourceBundle(claimed.resourceBundleRef ?? null, resourceMaterializationEnv(options.env ?? process.env, options.runId, attemptId, claimed.workspaceRef));
|
||||
if (materialized) {
|
||||
workspacePath = materialized.workspacePath;
|
||||
resourceEnv = resourceEnvForMaterialized(options.env ?? process.env, materialized);
|
||||
@@ -145,8 +145,14 @@ function withResourceAssembly(options: RunnerOnceOptions, resourceEnv: NodeJS.Pr
|
||||
};
|
||||
}
|
||||
|
||||
function resourceMaterializationEnv(env: NodeJS.ProcessEnv, runId: string, attemptId: string): NodeJS.ProcessEnv {
|
||||
return { ...env, AGENTRUN_RUN_ID: env.AGENTRUN_RUN_ID ?? runId, AGENTRUN_ATTEMPT_ID: env.AGENTRUN_ATTEMPT_ID ?? attemptId };
|
||||
function resourceMaterializationEnv(env: NodeJS.ProcessEnv, runId: string, attemptId: string, workspaceRef: RunRecord["workspaceRef"]): NodeJS.ProcessEnv {
|
||||
const workspaceBranch = typeof workspaceRef.branch === "string" && workspaceRef.branch.trim().length > 0 ? workspaceRef.branch.trim() : undefined;
|
||||
return {
|
||||
...env,
|
||||
AGENTRUN_RUN_ID: env.AGENTRUN_RUN_ID ?? runId,
|
||||
AGENTRUN_ATTEMPT_ID: env.AGENTRUN_ATTEMPT_ID ?? attemptId,
|
||||
...(workspaceBranch ? { AGENTRUN_WORKSPACE_BRANCH: env.AGENTRUN_WORKSPACE_BRANCH ?? workspaceBranch, AGENTRUN_WORKSPACE_REF: env.AGENTRUN_WORKSPACE_REF ?? workspaceBranch } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function resourceEnvForMaterialized(env: NodeJS.ProcessEnv, materialized: Awaited<ReturnType<typeof materializeResourceBundle>>): NodeJS.ProcessEnv | undefined {
|
||||
|
||||
@@ -11,7 +11,8 @@ 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; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"] };
|
||||
type LocalBundle = { repoUrl: string; commitId: string; branch?: string; bundles?: ResourceBundleRef["bundles"]; promptRefs?: ResourceBundleRef["promptRefs"] };
|
||||
type RefResolvedBundle = LocalBundle & { branch: string; latestCommitId: string };
|
||||
|
||||
const selfTest: SelfTestCase = async (context) => {
|
||||
const containerfile = await readFile(path.join(context.root, "deploy/container/Containerfile"), "utf8");
|
||||
@@ -95,6 +96,21 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
assert.deepEqual(((materialized.skillDirs as JsonRecord).names), ["hwpod-cli", "hwpod-ctl"]);
|
||||
assertNoSecretLeak(resultEnvelope);
|
||||
|
||||
const refBundle = await createRefResolvedLocalGitBundle(context);
|
||||
const refRun = await createHwlabRun(client, context, refBundle, "hwlab-session-ref-resolution", "resolve bundle from branch ref", "hwlab-command-ref-resolution");
|
||||
const refRunResult = await runOnce({ managerUrl: server.baseUrl, runId: refRun.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-ref-resolution") }, oneShot: true }) as JsonRecord;
|
||||
assert.equal(refRunResult.terminalStatus, "completed");
|
||||
const refEnvelope = await client.get(`/api/v1/runs/${refRun.runId}/commands/${refRun.commandId}/result`) as JsonRecord;
|
||||
const refResource = refEnvelope.resourceBundleRef as JsonRecord;
|
||||
assert.equal(refResource.commitId, refBundle.commitId, "result summary keeps the request commit as a non-authoritative hint");
|
||||
const refMaterialized = refResource.materialized as JsonRecord;
|
||||
assert.equal(refMaterialized.commitId, refBundle.latestCommitId, "materialized bundle must resolve the current workspaceRef.branch commit");
|
||||
assert.equal(refMaterialized.requestedCommitId, refBundle.commitId);
|
||||
assert.equal(refMaterialized.requestedRef, refBundle.branch);
|
||||
const refBundleCommits = (((refMaterialized.bundles as JsonRecord).items as JsonRecord[]).map((item) => item.commitId));
|
||||
assert.deepEqual(refBundleCommits, [refBundle.latestCommitId, refBundle.latestCommitId]);
|
||||
assertNoSecretLeak(refEnvelope);
|
||||
|
||||
const assemblyRun = await createHwlabRun(client, context, assemblyBundle, "hwlab-session-assembly", "list visible bundle skills without tools", "hwlab-command-assembly-1");
|
||||
const assemblyInputFile = path.join(context.tmp, "fake-codex-turn-input-assembly.jsonl");
|
||||
const assemblyRunner = runOnce({ managerUrl: server.baseUrl, runId: assemblyRun.runId, commandId: assemblyRun.commandId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_WORKSPACE_ROOT: path.join(context.tmp, "workspaces-assembly"), AGENTRUN_FAKE_CODEX_TURN_INPUT_FILE: assemblyInputFile }, idleTimeoutMs: 500, pollIntervalMs: 50 });
|
||||
@@ -195,14 +211,14 @@ 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-tools-path", "gitbundle-skill-dir-assembly", "resource-prompt-required-blocker", "same-run-runner-multiturn", "running-steer", "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", "same-run-runner-multiturn", "running-steer", "running-cancel"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
};
|
||||
|
||||
async function createLocalGitBundle(context: SelfTestContext): Promise<LocalBundle> {
|
||||
const repo = path.join(context.tmp, "bundle-repo");
|
||||
async function createLocalGitBundle(context: SelfTestContext, repoName = "bundle-repo"): Promise<LocalBundle> {
|
||||
const repo = path.join(context.tmp, repoName);
|
||||
await mkdir(repo, { recursive: true });
|
||||
await execFile("git", ["init"], { cwd: repo });
|
||||
await writeFile(path.join(repo, "README.md"), "HWLAB bundle self-test\n", "utf8");
|
||||
@@ -244,13 +260,26 @@ async function createLocalGitBundle(context: SelfTestContext): Promise<LocalBund
|
||||
return { repoUrl: repo, commitId: stdout.trim() };
|
||||
}
|
||||
|
||||
async function createRefResolvedLocalGitBundle(context: SelfTestContext): Promise<RefResolvedBundle> {
|
||||
const first = await createLocalGitBundle(context, "bundle-ref-repo");
|
||||
const { stdout: branchStdout } = await execFile("git", ["branch", "--show-current"], { cwd: first.repoUrl });
|
||||
const branch = branchStdout.trim() || "master";
|
||||
await writeFile(path.join(first.repoUrl, "README.md"), "HWLAB bundle self-test latest ref\n", "utf8");
|
||||
await execFile("git", ["add", "README.md"], { cwd: first.repoUrl });
|
||||
await execFile("git", ["-c", "user.email=selftest@example.invalid", "-c", "user.name=AgentRun SelfTest", "commit", "-m", "bundle selftest latest ref"], { cwd: first.repoUrl });
|
||||
const { stdout } = await execFile("git", ["rev-parse", "HEAD"], { cwd: first.repoUrl });
|
||||
const latestCommitId = stdout.trim();
|
||||
assert.notEqual(latestCommitId, first.commitId);
|
||||
return { ...first, branch, latestCommitId };
|
||||
}
|
||||
|
||||
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;
|
||||
const run = await client.post("/api/v1/runs", {
|
||||
tenantId: "hwlab",
|
||||
projectId: "pikasTech/HWLAB",
|
||||
workspaceRef: { kind: "opaque", repo: "pikasTech/HWLAB" },
|
||||
workspaceRef: { kind: "opaque", repo: "pikasTech/HWLAB", ...(bundle.branch ? { branch: bundle.branch } : {}) },
|
||||
sessionRef: { sessionId, conversationId: sessionId },
|
||||
resourceBundleRef,
|
||||
providerId: "G14",
|
||||
|
||||
Reference in New Issue
Block a user