Skip to content
Closed
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
123 changes: 123 additions & 0 deletions tests/prompts/feature-flag-rollout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { describe, it, expect } from "vitest";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
import { registerFeatureFlagRolloutPrompt } from "../../src/prompts/feature-flag-rollout.js";

async function createTestClient(): Promise<Client> {
const server = new McpServer(
{ name: "test-server", version: "0.0.1" },
{ capabilities: { prompts: {} } },
);
registerFeatureFlagRolloutPrompt(server);

const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
const client = new Client({ name: "test-client", version: "0.0.1" });

await Promise.all([
client.connect(clientTransport),
server.connect(serverTransport),
]);

return client;
}

async function getPromptText(args: Record<string, string>): Promise<string> {
const client = await createTestClient();
const result = await client.getPrompt({
name: "feature-flag-rollout",
arguments: args,
});
return (result.messages[0].content as { type: string; text: string }).text;
}

describe("feature-flag-rollout prompt", () => {
it("appears in the prompt list with rollout workflow description", async () => {
const client = await createTestClient();
const { prompts } = await client.listPrompts();

const prompt = prompts.find((p) => p.name === "feature-flag-rollout");
expect(prompt).toBeDefined();
expect(prompt!.description).toContain("progressive FME feature flag rollout");
});

it("documents workspaceId, orgId, and projectId as optional dual-mode scope args", async () => {
const client = await createTestClient();
const { prompts } = await client.listPrompts();
const prompt = prompts.find((p) => p.name === "feature-flag-rollout")!;

const argNames = prompt.arguments!.map((a) => a.name);
expect(argNames).toEqual(
expect.arrayContaining(["featureFlagName", "workspaceId", "orgId", "projectId"]),
);

const workspaceId = prompt.arguments!.find((a) => a.name === "workspaceId")!;
const orgId = prompt.arguments!.find((a) => a.name === "orgId")!;
const projectId = prompt.arguments!.find((a) => a.name === "projectId")!;

expect(workspaceId.required).toBeFalsy();
expect(orgId.required).toBeFalsy();
expect(projectId.required).toBeFalsy();
});

it("requires featureFlagName", async () => {
const client = await createTestClient();
const { prompts } = await client.listPrompts();
const prompt = prompts.find((p) => p.name === "feature-flag-rollout")!;

const featureFlagName = prompt.arguments!.find((a) => a.name === "featureFlagName")!;
expect(featureFlagName.required).toBe(true);
});

it("rejects when neither workspaceId nor orgId+projectId are provided", async () => {
const client = await createTestClient();

await expect(
client.getPrompt({
name: "feature-flag-rollout",
arguments: { featureFlagName: "my_flag" },
}),
).rejects.toThrow(/Provide either workspaceId.*or orgId \+ projectId/);
});

it("rejects partial Harness-native scope (orgId only)", async () => {
const client = await createTestClient();

await expect(
client.getPrompt({
name: "feature-flag-rollout",
arguments: { featureFlagName: "my_flag", orgId: "default" },
}),
).rejects.toThrow(/Provide either workspaceId.*or orgId \+ projectId/);
});

it("legacy mode: interpolates workspace_id scope args and omits Harness-native caveat", async () => {
const text = await getPromptText({
featureFlagName: "checkout_v2",
workspaceId: "ws-legacy-1",
});

expect(text).toContain('feature flag "checkout_v2"');
expect(text).toContain('workspace_id="ws-legacy-1"');
expect(text).not.toContain('org_id=');
expect(text).not.toContain("Harness-native mode");
expect(text).toContain('resource_type="fme_feature_flag"');
expect(text).toContain('action="kill"');
expect(text).toContain('action="restore"');
});

it("Harness-native mode: interpolates org_id/project_id and surfaces NYI caveat", async () => {
const text = await getPromptText({
featureFlagName: "checkout_v2",
orgId: "default",
projectId: "payments",
});

expect(text).toContain('org_id="default", project_id="payments"');
expect(text).not.toContain('workspace_id=');
expect(text).toContain("Harness-native mode (org_id/project_id)");
expect(text).toContain("fme_feature_flag_definition");
expect(text).toContain("fme_rollout_status");
expect(text).toContain("steps 3, 4, and 7");
});
});
90 changes: 90 additions & 0 deletions tests/registry/governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,94 @@ describe("policy and policy_set multi-scope support", () => {
expect(mockRequest).not.toHaveBeenCalled();
},
);

it.each(["policy", "policy_set"] as const)(
"%s get omits org/project for resource_scope=account",
async (resourceType) => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "governance" }));
const mockRequest = vi.fn().mockResolvedValue({ identifier: "acct-policy" });
const client = makeClient(mockRequest);
const idField = resourceType === "policy" ? "policy_id" : "policy_set_id";

await registry.dispatch(client, resourceType, "get", {
resource_scope: "account",
[idField]: "acct-policy",
});

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

it.each(["policy", "policy_set"] as const)(
"%s create omits org/project for resource_scope=account",
async (resourceType) => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "governance" }));
const mockRequest = vi.fn().mockResolvedValue({ identifier: "new-policy" });
const client = makeClient(mockRequest);

await registry.dispatch(client, resourceType, "create", {
resource_scope: "account",
body: {
identifier: "new-policy",
name: "New Policy",
...(resourceType === "policy"
? { rego: "package harness\n\ndefault allow = true" }
: {
action: "onstep",
type: "sbom",
enabled: true,
}),
},
});

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

it.each(["policy", "policy_set"] as const)(
"%s update injects only org for resource_scope=org",
async (resourceType) => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "governance" }));
const mockRequest = vi.fn().mockResolvedValue({ identifier: "org-policy" });
const client = makeClient(mockRequest);
const idField = resourceType === "policy" ? "policy_id" : "policy_set_id";

await registry.dispatch(client, resourceType, "update", {
resource_scope: "org",
org_id: "platform",
[idField]: "org-policy",
body: { name: "Updated" },
});

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

it.each(["policy", "policy_set"] as const)(
"%s delete keeps project default when resource_scope is omitted",
async (resourceType) => {
const registry = new Registry(makeConfig({ HARNESS_TOOLSETS: "governance" }));
const mockRequest = vi.fn().mockResolvedValue({});
const client = makeClient(mockRequest);
const idField = resourceType === "policy" ? "policy_id" : "policy_set_id";

await registry.dispatch(client, resourceType, "delete", {
[idField]: "proj-policy",
});

const call = mockRequest.mock.calls[0][0] as { params: Record<string, unknown>; method: string };
expect(call.method).toBe("DELETE");
expect(call.params.orgIdentifier).toBe("default");
expect(call.params.projectIdentifier).toBe("test-project");
},
);
});
46 changes: 46 additions & 0 deletions tests/registry/infrastructure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,50 @@ describe("infrastructure deep links", () => {

expect(result.openInHarness).toBe(INFRA_DEEP_LINK);
});

it("get: openInHarness falls back to environment_id param when response omits environmentRef", async () => {
const client = makeClient(
vi.fn().mockResolvedValue({
data: {
identifier: "k8s",
name: "k8s",
orgIdentifier: "default",
projectIdentifier: "avi",
},
}),
);
const result = (await registry.dispatch(client, "infrastructure", "get", {
infrastructure_id: "k8s",
org_id: "default",
project_id: "avi",
environment_id: "preprod",
})) as Record<string, unknown>;

expect(result.openInHarness).toBe(INFRA_DEEP_LINK);
});

it("list: openInHarness aliases nested infrastructure.environmentRef onto environmentIdentifier", async () => {
const client = makeClient(
vi.fn().mockResolvedValue({
data: {
content: [
{
identifier: "k8s",
name: "k8s",
infrastructure: { environmentRef: "preprod" },
orgIdentifier: "default",
projectIdentifier: "avi",
},
],
totalElements: 1,
},
}),
);
const result = (await registry.dispatch(client, "infrastructure", "list", {
org_id: "default",
project_id: "avi",
})) as { items: Array<Record<string, unknown>> };

expect(result.items[0]!.openInHarness).toBe(INFRA_DEEP_LINK);
});
});
Loading