feat: 支持 v0.1 deepseek backend profile
This commit is contained in:
@@ -13,7 +13,7 @@ const selfTest: SelfTestCase = async () => {
|
||||
(error) => error instanceof AgentRunError && error.failureKind === "infra-failed" && error.message.includes("DATABASE_URL is required"),
|
||||
);
|
||||
const postgresContract = postgresMigrationContract();
|
||||
assert.equal(postgresContract.latestMigrationId, "001_v01_initial_durable_store");
|
||||
assert.equal(postgresContract.latestMigrationId, "002_v01_backend_profiles");
|
||||
assert.ok(Array.isArray(postgresContract.requiredTables));
|
||||
assert.ok(postgresContract.requiredTables.includes("agentrun_schema_migrations"));
|
||||
assert.ok(postgresContract.requiredTables.includes("agentrun_runs"));
|
||||
|
||||
@@ -25,9 +25,22 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(rendered.mutation, false);
|
||||
assert.equal(((rendered.retention as JsonRecord).ttlSecondsAfterFinished), 86_400);
|
||||
assert.equal((rendered.jobIdentity as { serviceAccountName?: string }).serviceAccountName, "agentrun-v01-runner");
|
||||
assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome);
|
||||
assertRunnerJobUsesWritableCodexHome(rendered.manifest as JsonRecord, context.codexHome, "codex-0", "/var/run/agentrun/secrets/codex-0");
|
||||
assertNoSecretLeak(rendered);
|
||||
|
||||
const deepseekItem = await createRunWithCommand(client, { ...context, backendProfile: "deepseek" }, "deepseek job smoke", "selftest-deepseek-job-render", 15_000);
|
||||
const deepseekRendered = renderRunnerJobDryRun({
|
||||
run: await client.get(`/api/v1/runs/${deepseekItem.runId}`) as RunRecord,
|
||||
commandId: deepseekItem.commandId,
|
||||
managerUrl: server.baseUrl,
|
||||
image: "127.0.0.1:5000/agentrun/agentrun-mgr@sha256:1111111111111111111111111111111111111111111111111111111111111111",
|
||||
attemptId: "attempt_selftest_deepseek",
|
||||
sourceCommit: "self-test",
|
||||
});
|
||||
assertRunnerJobUsesWritableCodexHome(deepseekRendered.manifest as JsonRecord, context.deepseekHome, "deepseek-0", "/var/run/agentrun/secrets/deepseek-0");
|
||||
assertRunnerJobDoesNotMountProfile(deepseekRendered.manifest as JsonRecord, "codex-0");
|
||||
assertNoSecretLeak(deepseekRendered);
|
||||
|
||||
const fakeKubectl = path.join(context.tmp, "fake-kubectl.js");
|
||||
const createdManifest = path.join(context.tmp, "created-runner-job.json");
|
||||
await writeFile(fakeKubectl, `#!/usr/bin/env bun
|
||||
@@ -64,7 +77,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => serverWithKubectl.server.close(() => resolve()));
|
||||
}
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl"] };
|
||||
return { name: "runner-k8s-job", tests: ["runner-k8s-job-dry-run", "runner-k8s-job-deepseek-profile-dry-run", "runner-k8s-job-create-api", "runner-k8s-job-retention-ttl"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
@@ -72,7 +85,7 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
|
||||
|
||||
export default selfTest;
|
||||
|
||||
function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCodexHome: string): void {
|
||||
function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCodexHome: string, volumeName: string, projectionPath: string): void {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
const podSpec = template.spec as JsonRecord;
|
||||
@@ -83,12 +96,24 @@ function assertRunnerJobUsesWritableCodexHome(manifest: JsonRecord, expectedCode
|
||||
const runner = containers[0] as JsonRecord;
|
||||
const mounts = runner.volumeMounts as JsonRecord[];
|
||||
assert.ok(mounts.some((mount) => mount.name === "runner-home" && mount.mountPath === "/home/agentrun"), "runner-home must mount at /home/agentrun");
|
||||
assert.ok(mounts.some((mount) => mount.name === "codex-0" && mount.mountPath === "/var/run/agentrun/secrets/codex-0" && mount.readOnly === true), "Codex Secret must mount read-only outside CODEX_HOME");
|
||||
assert.ok(mounts.some((mount) => mount.name === volumeName && mount.mountPath === projectionPath && mount.readOnly === true), "Codex Secret must mount read-only outside CODEX_HOME");
|
||||
|
||||
const env = runner.env as JsonRecord[];
|
||||
const value = (name: string): unknown => env.find((item) => item.name === name)?.value;
|
||||
assert.equal(value("HOME"), "/home/agentrun");
|
||||
assert.equal(value("CODEX_HOME"), expectedCodexHome);
|
||||
assert.equal(value("AGENTRUN_CODEX_SECRET_HOME"), "/var/run/agentrun/secrets/codex-0");
|
||||
assert.equal(value("AGENTRUN_CODEX_SECRET_HOME"), projectionPath);
|
||||
assert.notEqual(value("CODEX_HOME"), value("AGENTRUN_CODEX_SECRET_HOME"));
|
||||
}
|
||||
|
||||
function assertRunnerJobDoesNotMountProfile(manifest: JsonRecord, volumeName: string): void {
|
||||
const spec = manifest.spec as JsonRecord;
|
||||
const template = spec.template as JsonRecord;
|
||||
const podSpec = template.spec as JsonRecord;
|
||||
const volumes = podSpec.volumes as JsonRecord[];
|
||||
const containers = podSpec.containers as JsonRecord[];
|
||||
const runner = containers[0] as JsonRecord;
|
||||
const mounts = runner.volumeMounts as JsonRecord[];
|
||||
assert.equal(volumes.some((volume) => volume.name === volumeName), false, `${volumeName} volume must not be mounted for another backendProfile`);
|
||||
assert.equal(mounts.some((mount) => mount.name === volumeName), false, `${volumeName} mount must not exist for another backendProfile`);
|
||||
}
|
||||
|
||||
@@ -33,6 +33,21 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await access(path.join(projectedHome, "auth.json"));
|
||||
await access(path.join(projectedHome, "config.toml"));
|
||||
|
||||
const deepseekHome = path.join(context.tmp, "runtime-deepseek-home");
|
||||
const deepseek = await createRunWithCommand(client, { ...context, backendProfile: "deepseek" }, "hello deepseek", "selftest-deepseek-turn", 15_000);
|
||||
const deepseekResult = await runOnce({ managerUrl: server.baseUrl, runId: deepseek.runId, backendProfile: "deepseek", codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: deepseekHome, env: { CODEX_HOME: deepseekHome, AGENTRUN_CODEX_SECRET_HOME: context.deepseekHome } });
|
||||
assert.equal(deepseekResult.terminalStatus, "completed");
|
||||
await access(path.join(deepseekHome, "auth.json"));
|
||||
await access(path.join(deepseekHome, "config.toml"));
|
||||
const deepseekEvents = await client.get(`/api/v1/runs/${deepseek.runId}/events?afterSeq=0&limit=100`) as { items?: Array<{ type: string; payload: unknown }> };
|
||||
assert.ok(deepseekEvents.items?.some((event) => event.type === "backend_status" && JSON.stringify(event.payload).includes("deepseek")), "deepseek backend_status should include profile metadata");
|
||||
assertNoSecretLeak(deepseekEvents);
|
||||
|
||||
await assert.rejects(
|
||||
() => createRunWithCommand(client, { ...context, backendProfile: "deepseek", includeOnlyProfile: "codex" }, "missing deepseek", "selftest-deepseek-missing-secret", 15_000),
|
||||
(error) => error instanceof Error && error.message.includes("requires a matching provider credential"),
|
||||
);
|
||||
|
||||
const configModel = await createRunWithCommand(client, context, "hello config model", "selftest-config-model", 15_000);
|
||||
const configModelResult = await runOnce({ managerUrl: server.baseUrl, runId: configModel.runId, codexCommand: context.fakeCodexCommand, codexArgs: context.fakeCodexArgs, codexHome: context.codexHome, env: { CODEX_HOME: context.codexHome, AGENTRUN_FAKE_CODEX_MODE: "reject-unexpected-model" } });
|
||||
assert.equal(configModelResult.terminalStatus, "completed", "unspecified model should be omitted so Codex config.toml remains authoritative");
|
||||
@@ -50,7 +65,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
await runFailureCase({ client, managerUrl: server.baseUrl, context, mode: "missing-terminal", expectedStatus: "failed", expectedFailureKind: "backend-timeout", timeoutMs: 500 });
|
||||
await runSpawnFailureCase({ client, managerUrl: server.baseUrl, context });
|
||||
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-missing-turn-result", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-spawn-failure"] };
|
||||
return { name: "codex-stdio", tests: ["runner-lease-heartbeat", "codex-stdio-fake-turn", "codex-stdio-projected-writable-home", "codex-stdio-deepseek-profile-fake-turn", "codex-stdio-deepseek-missing-secret-no-fallback", "codex-stdio-config-model-authoritative", "codex-stdio-explicit-model-forwarded", "codex-stdio-missing-turn-result", "codex-stdio-provider-503-rpc-error", "codex-stdio-provider-503-terminal", "codex-stdio-provider-503-retry-event", "codex-stdio-invalid-json", "codex-stdio-timeout", "codex-stdio-spawn-failure"] };
|
||||
} finally {
|
||||
await new Promise<void>((resolve) => server.server.close(() => resolve()));
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
const secretPlan = await renderCodexProviderSecretPlan({ codexHome: context.codexHome, dryRun: true });
|
||||
assert.equal(secretPlan.namespace, "agentrun-v01");
|
||||
assert.equal(secretPlan.secretName, "agentrun-v01-provider-codex");
|
||||
assert.equal(secretPlan.profile, "codex");
|
||||
assert.deepEqual(secretPlan.keys, ["auth.json", "config.toml"]);
|
||||
assert.equal(secretPlan.writeAttempted, false);
|
||||
assert.equal(secretPlan.totalBytes, Buffer.byteLength(JSON.stringify({ token: "test-token-material" }), "utf8") + Buffer.byteLength("model = \"gpt-test\"\n", "utf8"));
|
||||
@@ -18,6 +19,12 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
assert.equal(renderedSecretJson.includes("gpt-test"), false);
|
||||
assert.equal(renderedSecretJson.includes("model ="), false);
|
||||
|
||||
const deepseekSecretPlan = await renderCodexProviderSecretPlan({ profile: "deepseek", codexHome: context.deepseekHome, dryRun: true });
|
||||
assert.equal(deepseekSecretPlan.secretName, "agentrun-v01-provider-deepseek");
|
||||
assert.equal(deepseekSecretPlan.profile, "deepseek");
|
||||
assert.equal(JSON.stringify(deepseekSecretPlan).includes("test-token-material-deepseek"), false);
|
||||
assert.equal(JSON.stringify(deepseekSecretPlan).includes("deepseek-test"), false);
|
||||
|
||||
await assert.rejects(
|
||||
() => renderCodexProviderSecretPlan({ codexHome: path.join(context.tmp, "missing-codex-home"), dryRun: true }),
|
||||
(error) => error instanceof AgentRunError && error.failureKind === "secret-unavailable",
|
||||
@@ -42,7 +49,7 @@ const selfTest: SelfTestCase = async (context) => {
|
||||
(error) => error instanceof AgentRunError && error.failureKind === "schema-invalid",
|
||||
);
|
||||
|
||||
return { name: "secret-render", tests: ["codex-secret-dry-run"] };
|
||||
return { name: "secret-render", tests: ["codex-secret-dry-run", "deepseek-secret-dry-run"] };
|
||||
};
|
||||
|
||||
export default selfTest;
|
||||
|
||||
+25
-4
@@ -3,12 +3,14 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import assert from "node:assert/strict";
|
||||
import { ManagerClient } from "../mgr/client.js";
|
||||
import type { JsonRecord } from "../common/types.js";
|
||||
import type { BackendProfile, JsonRecord } from "../common/types.js";
|
||||
import { backendProfileSpec } from "../common/backend-profiles.js";
|
||||
|
||||
export interface SelfTestContext {
|
||||
root: string;
|
||||
tmp: string;
|
||||
codexHome: string;
|
||||
deepseekHome: string;
|
||||
workspace: string;
|
||||
fakeCodexPath: string;
|
||||
fakeCodexCommand: string;
|
||||
@@ -26,17 +28,22 @@ export type SelfTestCase = (context: SelfTestContext) => Promise<SelfTestResult>
|
||||
export async function createSelfTestContext(root: string): Promise<SelfTestContext> {
|
||||
const tmp = await mkdtemp(path.join(os.tmpdir(), "agentrun-selftest-"));
|
||||
const codexHome = path.join(tmp, "codex-home");
|
||||
const deepseekHome = path.join(tmp, "deepseek-home");
|
||||
const workspace = path.join(tmp, "workspace");
|
||||
await mkdir(codexHome, { recursive: true });
|
||||
await mkdir(deepseekHome, { recursive: true });
|
||||
await mkdir(workspace, { recursive: true });
|
||||
await writeFile(path.join(codexHome, "auth.json"), JSON.stringify({ token: "test-token-material" }));
|
||||
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n");
|
||||
await writeFile(path.join(deepseekHome, "auth.json"), JSON.stringify({ token: "test-token-material-deepseek" }));
|
||||
await writeFile(path.join(deepseekHome, "config.toml"), "model = \"deepseek-test\"\n");
|
||||
await writeFile(path.join(workspace, "README.md"), "self-test workspace\n");
|
||||
const fakeCodexPath = path.join(root, "src/selftest/fake-codex-app-server.ts");
|
||||
return {
|
||||
root,
|
||||
tmp,
|
||||
codexHome,
|
||||
deepseekHome,
|
||||
workspace,
|
||||
fakeCodexPath,
|
||||
fakeCodexCommand: process.env.AGENTRUN_SELFTEST_CODEX_COMMAND ?? defaultFakeCommand(),
|
||||
@@ -45,19 +52,20 @@ export async function createSelfTestContext(root: string): Promise<SelfTestConte
|
||||
};
|
||||
}
|
||||
|
||||
export async function createRunWithCommand(client: ManagerClient, context: Pick<SelfTestContext, "workspace" | "codexHome">, prompt: string, idempotencyKey: string, timeoutMs: number): Promise<{ runId: string; commandId: string }> {
|
||||
export async function createRunWithCommand(client: ManagerClient, context: Pick<SelfTestContext, "workspace" | "codexHome"> & Partial<Pick<SelfTestContext, "deepseekHome">> & { backendProfile?: BackendProfile; includeOnlyProfile?: BackendProfile }, prompt: string, idempotencyKey: string, timeoutMs: number): Promise<{ runId: string; commandId: string }> {
|
||||
const backendProfile = context.backendProfile ?? "codex";
|
||||
const run = await client.post("/api/v1/runs", {
|
||||
tenantId: "unidesk",
|
||||
projectId: "pikasTech/unidesk",
|
||||
workspaceRef: { kind: "host-path", path: context.workspace },
|
||||
providerId: "G14",
|
||||
backendProfile: "codex",
|
||||
backendProfile,
|
||||
executionPolicy: {
|
||||
sandbox: "workspace-write",
|
||||
approval: "never",
|
||||
timeoutMs,
|
||||
network: "default",
|
||||
secretScope: { allowCredentialEcho: false, providerCredentials: [{ profile: "codex", secretRef: { name: "agentrun-v01-provider-codex", keys: ["auth.json", "config.toml"], mountPath: context.codexHome } }] },
|
||||
secretScope: { allowCredentialEcho: false, providerCredentials: providerCredentials(context, backendProfile) },
|
||||
},
|
||||
traceSink: null,
|
||||
}) as { id: string };
|
||||
@@ -67,9 +75,22 @@ export async function createRunWithCommand(client: ManagerClient, context: Pick<
|
||||
return { runId: run.id, commandId: command.id };
|
||||
}
|
||||
|
||||
function providerCredentials(context: Pick<SelfTestContext, "codexHome"> & Partial<Pick<SelfTestContext, "deepseekHome">> & { includeOnlyProfile?: BackendProfile }, backendProfile: BackendProfile): JsonRecord[] {
|
||||
const profiles: BackendProfile[] = context.includeOnlyProfile ? [context.includeOnlyProfile] : [backendProfile];
|
||||
return profiles.map((profile) => ({
|
||||
profile,
|
||||
secretRef: {
|
||||
name: backendProfileSpec(profile)?.defaultSecretName ?? `agentrun-v01-provider-${profile}`,
|
||||
keys: ["auth.json", "config.toml"],
|
||||
mountPath: profile === "deepseek" ? context.deepseekHome ?? context.codexHome : context.codexHome,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
export function assertNoSecretLeak(value: unknown): void {
|
||||
const text = JSON.stringify(value);
|
||||
assert.equal(text.includes("test-token-material"), false);
|
||||
assert.equal(text.includes("test-token-material-deepseek"), false);
|
||||
assert.equal(text.includes("Bearer test-token"), false);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user