Files
fusion/plugins/fusion-plugin-compound-engineering/src/__tests__/settings.test.ts
gsxdsm 05fe6e5e0c FN-6575: make CE stage gating opt-out
Make Compound Engineering stage launch gating opt-out so registered stages like debug remain launchable by default.

- Replace enabledStages settings schema with disabledStages defaults and docs.
- Derive launchable stages from the live registry minus explicit disabled opt-outs.
- Update orchestrator gating, exports, tests, and plugin docs for the new setting.
- Add a patch changeset for the Compound Engineering plugin behavior fix.

Files changed:
 .changeset/fn-6575-ce-stage-optout.md              |  5 ++++
 docs/plugins/compound-engineering.md               |  4 +--
 .../fusion-plugin-compound-engineering/README.md   |  9 ++++--
 .../manifest.json                                  |  8 +++---
 .../src/__tests__/manifest.test.ts                 |  2 +-
 .../src/__tests__/settings.test.ts                 | 33 +++++++++++++---------
 .../src/__tests__/skill-wiring.test.ts             | 21 +++++++++++++-
 .../src/index.ts                                   |  1 +
 .../src/session/orchestrator.ts                    |  9 ++++--
 .../src/settings.ts                                | 33 ++++++++++++++--------
 10 files changed, 87 insertions(+), 38 deletions(-)

Fusion-Task-Id: FN-6575

Fusion-Task-Lineage: 903732d7-4ffb-4c8f-ba3e-e517d88bc644
2026-06-17 14:52:55 -07:00

120 lines
4.8 KiB
TypeScript

import { describe, expect, it } from "vitest";
import type { PluginSettingType } from "@fusion/plugin-sdk";
import { listStages } from "../session/stage-registry.js";
import {
DEFAULT_DISABLED_STAGES,
DEFAULT_MODEL_ID,
DEFAULT_PROVIDER,
DEFAULT_RECONCILE_INTERVAL_MINUTES,
DEFAULT_RECONCILE_ON_HOOKS,
getDefaultModelId,
getDefaultProvider,
getDisabledStages,
getEnabledStages,
getReconcileIntervalMinutes,
getReconcileOnHooks,
settingsSchema,
} from "../settings.js";
const VALID_TYPES: PluginSettingType[] = ["string", "number", "boolean", "enum", "password", "array"];
describe("compound engineering plugin settings schema", () => {
it("uses only valid plugin setting types and labels", () => {
for (const [key, schema] of Object.entries(settingsSchema)) {
expect(VALID_TYPES).toContain(schema.type);
expect(typeof schema.label).toBe("string");
expect(schema.label?.trim().length).toBeGreaterThan(0);
if (schema.type === "enum") {
expect(Array.isArray(schema.enumValues)).toBe(true);
expect(schema.enumValues?.length ?? 0).toBeGreaterThan(0);
}
if (schema.type === "array") {
expect(schema.itemType).toBe("string");
}
expect(key.length).toBeGreaterThan(0);
}
});
it("exposes the expected keys grouped into Sessions and Sync", () => {
expect(Object.keys(settingsSchema).sort()).toEqual(
[
"defaultModelId",
"defaultProvider",
"disabledStages",
"reconcileIntervalMinutes",
"reconcileOnHooks",
].sort(),
);
expect(settingsSchema.defaultProvider.group).toBe("Sessions");
expect(settingsSchema.defaultModelId.group).toBe("Sessions");
expect(settingsSchema.disabledStages.group).toBe("Sessions");
expect(settingsSchema.reconcileOnHooks.group).toBe("Sync");
expect(settingsSchema.reconcileIntervalMinutes.group).toBe("Sync");
});
it("defaults disabledStages to no explicit opt-outs", () => {
expect(DEFAULT_DISABLED_STAGES).toEqual([]);
expect(settingsSchema.disabledStages.defaultValue).toEqual(DEFAULT_DISABLED_STAGES);
});
it("uses documented literal defaults", () => {
expect(settingsSchema.reconcileOnHooks.defaultValue).toBe(true);
expect(settingsSchema.reconcileIntervalMinutes.defaultValue).toBe(15);
expect(settingsSchema.defaultProvider.defaultValue).toBe("");
expect(settingsSchema.defaultModelId.defaultValue).toBe("");
});
it("returns defaults for empty settings", () => {
const empty = {};
expect(getDefaultProvider(empty)).toBeUndefined();
expect(DEFAULT_PROVIDER).toBe("");
expect(getDefaultModelId(empty)).toBeUndefined();
expect(DEFAULT_MODEL_ID).toBe("");
expect(getEnabledStages(empty)).toEqual(listStages().map((s) => s.stageId));
expect(getReconcileOnHooks(empty)).toBe(DEFAULT_RECONCILE_ON_HOOKS);
expect(getReconcileIntervalMinutes(empty)).toBe(DEFAULT_RECONCILE_INTERVAL_MINUTES);
});
it("returns configured values when provided", () => {
const populated = {
defaultProvider: "anthropic",
defaultModelId: "claude-opus",
disabledStages: ["plan"],
reconcileOnHooks: false,
reconcileIntervalMinutes: 30,
} satisfies Record<string, unknown>;
expect(getDefaultProvider(populated)).toBe("anthropic");
expect(getDefaultModelId(populated)).toBe("claude-opus");
expect(getDisabledStages(populated)).toEqual(["plan"]);
expect(getEnabledStages(populated)).toEqual(listStages().map((s) => s.stageId).filter((id) => id !== "plan"));
expect(getReconcileOnHooks(populated)).toBe(false);
expect(getReconcileIntervalMinutes(populated)).toBe(30);
});
it("clamps the reconcile cadence to at least one minute", () => {
expect(getReconcileIntervalMinutes({ reconcileIntervalMinutes: 0 })).toBe(1);
expect(getReconcileIntervalMinutes({ reconcileIntervalMinutes: -5 })).toBe(1);
expect(getReconcileIntervalMinutes({ reconcileIntervalMinutes: 7.9 })).toBe(7);
});
it("ignores stale enabledStages snapshots when deriving launchable stages", () => {
expect(getEnabledStages({ enabledStages: ["strategy", "ideate", "brainstorm", "plan", "work"] })).toEqual(
listStages().map((s) => s.stageId),
);
});
it("falls back to defaults for malformed values", () => {
const liveDefault = listStages().map((s) => s.stageId);
expect(getDisabledStages({ disabledStages: "not-an-array" })).toEqual([]);
expect(getDisabledStages({ disabledStages: [] })).toEqual([]);
expect(getEnabledStages({ disabledStages: "not-an-array" })).toEqual(liveDefault);
expect(getEnabledStages({ disabledStages: [] })).toEqual(liveDefault);
expect(getDefaultProvider({ defaultProvider: " " })).toBeUndefined();
expect(getReconcileOnHooks({ reconcileOnHooks: "yes" })).toBe(DEFAULT_RECONCILE_ON_HOOKS);
});
});