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
134 changes: 134 additions & 0 deletions tests/prompts/feature-flag-rollout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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("rejects partial Harness-native scope (projectId only)", async () => {
const client = await createTestClient();

await expect(
client.getPrompt({
name: "feature-flag-rollout",
arguments: { featureFlagName: "my_flag", projectId: "payments" },
}),
).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)");

Check failure on line 129 in tests/prompts/feature-flag-rollout.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test (22)

tests/prompts/feature-flag-rollout.test.ts > feature-flag-rollout prompt > Harness-native mode: interpolates org_id/project_id and surfaces NYI caveat

AssertionError: expected 'Plan a progressive rollout for FME fe…' to contain 'Harness-native mode (org_id/project_i…' - Expected + Received - Harness-native mode (org_id/project_id) + Plan a progressive rollout for FME feature flag "checkout_v2" (org_id="default", project_id="payments"). + + Steps: + 1. **Get flag details**: Call harness_get with resource_type="fme_feature_flag", feature_flag_name="checkout_v2", org_id="default", project_id="payments" to see the current flag state + 2. **List environments**: Call harness_list with resource_type="fme_environment", org_id="default", project_id="payments" to see available environments + 3. **Get flag definition per environment**: For each environment, call harness_get with resource_type="fme_feature_flag_definition", feature_flag_name="checkout_v2", org_id="default", project_id="payments", environment_id=<env_id> to see treatments and rules + 4. **Check rollout statuses**: Call harness_list with resource_type="fme_rollout_status", org_id="default", project_id="payments" for rollout status context + 5. **Propose rollout plan**: Recommend a progressive rollout strategy: + - Phase 1: Restore flag in dev/test environments + - Phase 2: Restore in staging, verify treatments + - Phase 3: Restore in production + - Phase 4: Full production rollout + 6. **Safety gates**: Identify metrics or health checks between each phase + 7. **Rollback plan**: Use kill action to immediately turn off the flag if issues arise + + Present the rollout plan for review. Use harness_execute with resource_type="fme_feature_flag", action="kill" or action="restore", org_id="default", project_id="payments", feature_flag_name="checkout_v2", environment_id=<env_id> to execute each phase after user approval. ❯ tests/prompts/feature-flag-rollout.test.ts:129:18

Check failure on line 129 in tests/prompts/feature-flag-rollout.test.ts

View workflow job for this annotation

GitHub Actions / build-and-test (20)

tests/prompts/feature-flag-rollout.test.ts > feature-flag-rollout prompt > Harness-native mode: interpolates org_id/project_id and surfaces NYI caveat

AssertionError: expected 'Plan a progressive rollout for FME fe…' to contain 'Harness-native mode (org_id/project_i…' - Expected + Received - Harness-native mode (org_id/project_id) + Plan a progressive rollout for FME feature flag "checkout_v2" (org_id="default", project_id="payments"). + + Steps: + 1. **Get flag details**: Call harness_get with resource_type="fme_feature_flag", feature_flag_name="checkout_v2", org_id="default", project_id="payments" to see the current flag state + 2. **List environments**: Call harness_list with resource_type="fme_environment", org_id="default", project_id="payments" to see available environments + 3. **Get flag definition per environment**: For each environment, call harness_get with resource_type="fme_feature_flag_definition", feature_flag_name="checkout_v2", org_id="default", project_id="payments", environment_id=<env_id> to see treatments and rules + 4. **Check rollout statuses**: Call harness_list with resource_type="fme_rollout_status", org_id="default", project_id="payments" for rollout status context + 5. **Propose rollout plan**: Recommend a progressive rollout strategy: + - Phase 1: Restore flag in dev/test environments + - Phase 2: Restore in staging, verify treatments + - Phase 3: Restore in production + - Phase 4: Full production rollout + 6. **Safety gates**: Identify metrics or health checks between each phase + 7. **Rollback plan**: Use kill action to immediately turn off the flag if issues arise + + Present the rollout plan for review. Use harness_execute with resource_type="fme_feature_flag", action="kill" or action="restore", org_id="default", project_id="payments", feature_flag_name="checkout_v2", environment_id=<env_id> to execute each phase after user approval. ❯ tests/prompts/feature-flag-rollout.test.ts:129:18
expect(text).toContain("fme_feature_flag_definition");
expect(text).toContain("fme_rollout_status");
expect(text).toContain("steps 3, 4, and 7");
});
});
151 changes: 151 additions & 0 deletions tests/registry/governance.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,4 +496,155 @@ 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 get 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, "get", {
resource_scope: "org",
org_id: "platform",
[idField]: "org-policy",
});

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

it.each(["policy", "policy_set"] as const)(
"%s delete omits org/project for resource_scope=account",
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", {
resource_scope: "account",
[idField]: "acct-policy",
});

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

it.each(["policy", "policy_set"] as const)(
"%s delete injects only org for resource_scope=org",
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", {
resource_scope: "org",
org_id: "platform",
[idField]: "org-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("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");
},
);
});
72 changes: 72 additions & 0 deletions tests/registry/infrastructure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,4 +203,76 @@ 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);
});

it("update: openInHarness aliases nested infrastructure.environmentRef onto environmentIdentifier", async () => {
const client = makeClient(
vi.fn().mockResolvedValue({
data: {
identifier: "k8s",
name: "k8s",
infrastructure: { environmentRef: "preprod" },
orgIdentifier: "default",
projectIdentifier: "avi",
},
}),
);
const result = (await registry.dispatch(client, "infrastructure", "update", {
infrastructure_id: "k8s",
org_id: "default",
project_id: "avi",
body: {
name: "k8s",
type: "KubernetesDirect",
environmentRef: "preprod",
},
})) as Record<string, unknown>;

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