Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions tests/registry/repositories-file-scope.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
/**
* Regression tests for Harness Code file_content scope forwarding (#602).
* Project-scoped repositories require orgIdentifier/projectIdentifier on
* content and blame APIs — without them the Code service cannot resolve the repo.
*/
import { describe, expect, it, vi } from "vitest";
import type { Config } from "../../src/config.js";
import type { HarnessClient } from "../../src/client/harness-client.js";
import { Registry } from "../../src/registry/index.js";

function makeConfig(overrides: Partial<Config> = {}): Config {
return {
HARNESS_API_KEY: "pat.test",
HARNESS_ACCOUNT_ID: "test-account",
HARNESS_BASE_URL: "https://app.harness.io",
HARNESS_ORG: "default",
HARNESS_PROJECT: "test-project",
HARNESS_API_TIMEOUT_MS: 30000,
HARNESS_MAX_RETRIES: 3,
HARNESS_MAX_BODY_SIZE_MB: 10,
HARNESS_RATE_LIMIT_RPS: 10,
HARNESS_READ_ONLY: false,
HARNESS_SKIP_ELICITATION: false,
HARNESS_ALLOW_HTTP: false,
HARNESS_FME_BASE_URL: "https://api.split.io",
LOG_LEVEL: "info",
...overrides,
};
}

function makeClient(requestFn: (...args: unknown[]) => unknown): HarnessClient {
return {
request: requestFn,
account: "test-account",
} as unknown as HarnessClient;
}

describe("file_content scope query params", () => {
it("forwards org_id and project_id on get for project-scoped repos", async () => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "repositories" }));
const mockRequest = vi.fn().mockResolvedValue({ type: "file", content: "hello" });
const client = makeClient(mockRequest);

await registry.dispatch(client, "file_content", "get", {
repo_id: "my-repo",
path: "README.md",
git_ref: "main",
org_id: "AI_Devops",
project_id: "Sanity",
});

expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
method: "GET",
path: "/code/api/v1/repos/my-repo/content/README.md",
params: expect.objectContaining({
orgIdentifier: "AI_Devops",
projectIdentifier: "Sanity",
git_ref: "main",
}),
}));
});

it("omits org/project query params when scope is not provided (account-scoped repo)", async () => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "repositories" }));
const mockRequest = vi.fn().mockResolvedValue({ type: "file", content: "hello" });
const client = makeClient(mockRequest);

await registry.dispatch(client, "file_content", "get", {
repo_id: "account-repo",
path: "src/index.ts",
});

const call = mockRequest.mock.calls[0]![0] as { params: Record<string, unknown> };
expect(call.params.orgIdentifier).toBeUndefined();
expect(call.params.projectIdentifier).toBeUndefined();
});

it("forwards org_id and project_id on blame execute action", async () => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "repositories" }));
const mockRequest = vi.fn().mockResolvedValue({ lines: [] });
const client = makeClient(mockRequest);

await registry.dispatchExecute(client, "file_content", "blame", {
repo_id: "my-repo",
path: "src/main.go",
git_ref: "main",
line_from: 1,
line_to: 50,
org_id: "AI_Devops",
project_id: "Sanity",
});

expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
method: "GET",
path: "/code/api/v1/repos/my-repo/blame/src%2Fmain.go",
params: expect.objectContaining({
orgIdentifier: "AI_Devops",
projectIdentifier: "Sanity",
git_ref: "main",
line_from: 1,
line_to: 50,
}),
}));
});

it("uses explicit org_id/project_id over configured defaults for scoped repos", async () => {
const registry = new Registry(makeConfig({
HARNESS_TOOLSETS: "repositories",
HARNESS_ORG: "configured-org",
HARNESS_PROJECT: "configured-project",
}));
const mockRequest = vi.fn().mockResolvedValue({ type: "file", content: "data" });
const client = makeClient(mockRequest);

await registry.dispatch(client, "file_content", "get", {
repo_id: "scoped-repo",
path: "pkg/app.go",
org_id: "explicit-org",
project_id: "explicit-project",
});

expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({
params: expect.objectContaining({
orgIdentifier: "explicit-org",
projectIdentifier: "explicit-project",
}),
}));
});
});
67 changes: 67 additions & 0 deletions tests/schemas/schema-bundle-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,71 @@ describe("schema bundle contract", () => {
expect(cdSteps).toHaveProperty("DeployGoogleAgentRuntimeRevisionStepNode_template");
expect(cdSteps).toHaveProperty("DeployGoogleAgentRuntimeRevisionStepInfo");
});

it("includes upstream DeployAwsAgentCoreRevision step definitions in v0 pipeline", () => {
const pipelineDefs = SCHEMAS.pipeline.definitions as Record<string, Record<string, unknown>>;
const cdSteps = pipelineDefs.pipeline.steps.cd as Record<string, unknown>;

expect(cdSteps).toHaveProperty("DeployAwsAgentCoreRevisionStepNode");
expect(cdSteps).toHaveProperty("DeployAwsAgentCoreRevisionStepInfo");

const stepNode = cdSteps.DeployAwsAgentCoreRevisionStepNode as {
properties: { type: { enum: string[] } };
};
expect(stepNode.properties.type.enum).toContain("DeployAwsAgentCoreRevision");

const stepInfo = cdSteps.DeployAwsAgentCoreRevisionStepInfo as {
properties: Record<string, unknown>;
};
expect(stepInfo.properties).toHaveProperty("connectorRef");
expect(stepInfo.properties).toHaveProperty("image");
});

it("includes upstream DeployAwsAgentCoreRevision step definitions in v0 template", () => {
const templateDefs = SCHEMAS.template.definitions as Record<string, Record<string, unknown>>;
const cdSteps = templateDefs.pipeline.steps.cd as Record<string, unknown>;

expect(cdSteps).toHaveProperty("DeployAwsAgentCoreRevisionStepNode");
expect(cdSteps).toHaveProperty("DeployAwsAgentCoreRevisionStepNode_template");
expect(cdSteps).toHaveProperty("DeployAwsAgentCoreRevisionStepInfo");
});

it("includes upstream IdentitiesConfig and IdentitySpec in v0 pipeline common definitions", () => {
const common = (SCHEMAS.pipeline.definitions as Record<string, Record<string, unknown>>).pipeline
.common as Record<string, unknown>;

expect(common).toHaveProperty("IdentitiesConfig");
expect(common).toHaveProperty("IdentitySpec");

const identitiesConfig = common.IdentitiesConfig as {
maxProperties: number;
additionalProperties: { $ref: string };
};
expect(identitiesConfig.maxProperties).toBe(10);
expect(identitiesConfig.additionalProperties.$ref).toContain("IdentitySpec");

const identitySpec = common.IdentitySpec as {
properties: Record<string, { enum?: string[] }>;
};
expect(identitySpec.properties).toHaveProperty("audience");
expect(identitySpec.properties).toHaveProperty("subjectTemplate");
expect(identitySpec.properties.scope?.enum).toContain("STEP");
});

it("includes upstream DynamicStageNodeV1 in v1 pipeline and template unified stages", () => {
for (const key of ["pipeline_v1", "template_v1"] as const) {
const defs = SCHEMAS[key].definitions as Record<string, Record<string, unknown>>;
const unified = defs[key].stages.unified as Record<string, unknown>;

expect(unified).toHaveProperty("DynamicStageNodeV1");

const dynamicStage = unified.DynamicStageNodeV1 as {
required: string[];
properties: { dynamic: { properties: Record<string, unknown> } };
};
expect(dynamicStage.required).toContain("dynamic");
expect(dynamicStage.properties.dynamic.properties).toHaveProperty("source");
expect(dynamicStage.properties.dynamic.properties).toHaveProperty("source-config");
}
});
});
48 changes: 48 additions & 0 deletions tests/tools/harness-schema-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,54 @@ describe("harness_schema nested static definition lookup", () => {
const schema = parsed.schema as { properties?: Record<string, { description?: string }> };
expect(schema.properties?.user?.description).toContain("clone container");
});

it("resolves newly synced DeployAwsAgentCoreRevision step definition from v0 pipeline", async () => {
const server = makeMcpServer();
registerSchemaTool(server, undefined, undefined);
const result = await server.call("harness_schema", {
resource_type: "pipeline",
path: "DeployAwsAgentCoreRevisionStepNode",
});
const parsed = parseResult(result) as Record<string, unknown>;

expect(result.isError).toBeFalsy();
expect(parsed.path).toBe("steps.cd.DeployAwsAgentCoreRevisionStepNode");
expect(parsed.requested_path).toBe("DeployAwsAgentCoreRevisionStepNode");
const schema = parsed.schema as { properties?: { type?: { enum?: string[] } } };
expect(schema.properties?.type?.enum).toContain("DeployAwsAgentCoreRevision");
});

it("resolves IdentitiesConfig from v0 pipeline common definitions", async () => {
const server = makeMcpServer();
registerSchemaTool(server, undefined, undefined);
const result = await server.call("harness_schema", {
resource_type: "pipeline",
path: "IdentitiesConfig",
});
const parsed = parseResult(result) as Record<string, unknown>;

expect(result.isError).toBeFalsy();
expect(parsed.path).toBe("common.IdentitiesConfig");
expect(parsed.requested_path).toBe("IdentitiesConfig");
const schema = parsed.schema as { maxProperties?: number };
expect(schema.maxProperties).toBe(10);
});

it("resolves DynamicStageNodeV1 from v1 pipeline unified stages", async () => {
const server = makeMcpServer();
registerSchemaTool(server, undefined, undefined);
const result = await server.call("harness_schema", {
resource_type: "pipeline_v1",
path: "DynamicStageNodeV1",
});
const parsed = parseResult(result) as Record<string, unknown>;

expect(result.isError).toBeFalsy();
expect(parsed.path).toBe("stages.unified.DynamicStageNodeV1");
expect(parsed.requested_path).toBe("DynamicStageNodeV1");
const schema = parsed.schema as { required?: string[] };
expect(schema.required).toContain("dynamic");
});
});

// Wrapper definitions that are grouping objects (not schema nodes) must still
Expand Down
Loading