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
4 changes: 0 additions & 4 deletions src/prompts/feature-flag-rollout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,6 @@ export function registerFeatureFlagRolloutPrompt(server: McpServer): void {
? `workspace_id="${workspaceId}"`
: `org_id="${orgId}", project_id="${projectId}"`;

const nativeModeCaveat = workspaceId
? ""
: "\n\nNote: in Harness-native mode (org_id/project_id), fme_rollout_status is not yet implemented server-side and will error — step 4 only works today with workspace_id (legacy mode). Steps 3 and 7 work with org_id+project_id.";

return {
messages: [{
role: "user" as const,
Expand Down
131 changes: 131 additions & 0 deletions tests/prompts/feature-flag-rollout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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;
}

function promptText(result: Awaited<ReturnType<Client["getPrompt"]>>): string {
return (result.messages[0].content as { type: string; text: string }).text;
}

describe("feature-flag-rollout prompt", () => {
it("appears in the prompt list", 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("declares dual-mode scope arguments", 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 featureFlagName = prompt.arguments!.find((a) => a.name === "featureFlagName")!;
expect(featureFlagName.required).toBe(true);
});

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

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

it("uses legacy workspace_id scope in rollout instructions", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "feature-flag-rollout",
arguments: {
featureFlagName: "checkout_v2",
workspaceId: "ws-legacy-99",
},
});

const text = promptText(result);
expect(text).toContain('feature flag "checkout_v2"');
expect(text).toContain('workspace_id="ws-legacy-99"');
expect(text).not.toContain('org_id=');
expect(text).not.toContain('project_id=');
});

it("uses Harness-native org_id+project_id scope in rollout instructions", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "feature-flag-rollout",
arguments: {
featureFlagName: "checkout_v2",
orgId: "my-org",
projectId: "my-project",
},
});

const text = promptText(result);
expect(text).toContain('org_id="my-org", project_id="my-project"');
expect(text).not.toContain("workspace_id=");
});

it("guides agents through native FME rollout workflow resources", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "feature-flag-rollout",
arguments: {
featureFlagName: "dark_launch",
orgId: "o1",
projectId: "p1",
},
});

const text = promptText(result);
expect(text).toContain('resource_type="fme_feature_flag"');
expect(text).toContain('resource_type="fme_environment"');
expect(text).toContain('resource_type="fme_feature_flag_definition"');
expect(text).toContain('resource_type="fme_rollout_status"');
expect(text).toContain('action="kill" or action="restore"');
});

it("does not warn that native-mode fme_rollout_status is unavailable", async () => {
const client = await createTestClient();
const result = await client.getPrompt({
name: "feature-flag-rollout",
arguments: {
featureFlagName: "dark_launch",
orgId: "o1",
projectId: "p1",
},
});

const text = promptText(result);
expect(text).not.toMatch(/not yet implemented server-side/i);
expect(text).not.toMatch(/only works today with workspace_id/i);
});
});
14 changes: 14 additions & 0 deletions tests/schemas/schema-bundle-contract.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,20 @@ describe("schema bundle contract", () => {
expect(identitySpec.properties.scope?.enum).toContain("STEP");
});

it("includes upstream reuse field on DB schema apply step info in v0 pipeline and template", () => {
for (const key of ["pipeline", "template"] as const) {
const defs = SCHEMAS[key].definitions as Record<string, Record<string, unknown>>;
const stepInfo = defs.pipeline.steps.common.DBApplySchemaStepInfo as {
allOf: Array<{ properties?: Record<string, { oneOf?: unknown[] }> }>;
};

const properties = stepInfo.allOf.find((part) => part.properties)?.properties;
expect(properties).toBeDefined();
expect(properties).toHaveProperty("reuse");
expect(properties!.reuse.oneOf).toHaveLength(2);
}
});

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>>;
Expand Down
Loading