Add dynamic secrets experimental flag

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-06-25 21:48:16 +00:00
1 parent f90ea4dae4
commit f6f742d2f5
7 files changed
+111

No files matched your search

+1
View File
@@ -54,6 +54,7 @@ export interface InstanceExperimentalSettings {
enableExperimentalFileViewer: boolean;
enableCloudSync: boolean;
enableExternalObjects: boolean;
enableDynamicSecrets: boolean;
autoRestartDevServerWhenIdle: boolean;
enableIssueGraphLivenessAutoRecovery: boolean;
issueGraphLivenessAutoRecoveryLookbackHours: number;
@@ -48,6 +48,7 @@ export const instanceExperimentalSettingsSchema = z.object({
enableExperimentalFileViewer: z.boolean().default(false),
enableCloudSync: z.boolean().default(false),
enableExternalObjects: z.boolean().default(false),
enableDynamicSecrets: z.boolean().default(false),
autoRestartDevServerWhenIdle: z.boolean().default(false),
enableIssueGraphLivenessAutoRecovery: z.boolean().default(false),
issueGraphLivenessAutoRecoveryLookbackHours: z
@@ -81,6 +81,8 @@ describe("instance settings routes", () => {
enableIssuePlanDecompositions: false,
enableExperimentalFileViewer: false,
enableCloudSync: false,
enableExternalObjects: false,
enableDynamicSecrets: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
@@ -101,6 +103,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: false,
enableCloudSync: false,
enableExternalObjects: false,
enableDynamicSecrets: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
@@ -119,6 +122,8 @@ describe("instance settings routes", () => {
enableIssuePlanDecompositions: true,
enableExperimentalFileViewer: true,
enableCloudSync: true,
enableExternalObjects: false,
enableDynamicSecrets: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
@@ -144,6 +149,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: true,
enableCloudSync: true,
enableExternalObjects: false,
enableDynamicSecrets: true,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
@@ -197,6 +203,7 @@ describe("instance settings routes", () => {
enableTaskWatchdogs: false,
enableCloudSync: false,
enableExternalObjects: false,
enableDynamicSecrets: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 24,
@@ -292,6 +299,24 @@ describe("instance settings routes", () => {
});
});
it("allows local board users to update dynamic secrets", async () => {
const app = await createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
await request(app)
.patch("/api/instance/settings/experimental")
.send({ enableDynamicSecrets: true })
.expect(200);
expect(mockInstanceSettingsService.updateExperimental).toHaveBeenCalledWith({
enableDynamicSecrets: true,
});
});
it("allows local board users to update issue graph liveness auto-recovery", async () => {
const app = await createApp({
type: "board",
@@ -10,6 +10,7 @@ describe("instance settings service", () => {
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableCloudSync: true,
enableDynamicSecrets: true,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 48,
@@ -24,6 +25,7 @@ describe("instance settings service", () => {
enableExperimentalFileViewer: true,
enableTaskWatchdogs: true,
enableCloudSync: true,
enableDynamicSecrets: true,
autoRestartDevServerWhenIdle: true,
enableIssueGraphLivenessAutoRecovery: true,
issueGraphLivenessAutoRecoveryLookbackHours: 48,
@@ -47,6 +49,24 @@ describe("instance settings service", () => {
).toBe(false);
});
it("defaults enableDynamicSecrets to false for empty and legacy stored settings", () => {
expect(normalizeExperimentalSettings(undefined).enableDynamicSecrets).toBe(false);
expect(normalizeExperimentalSettings({}).enableDynamicSecrets).toBe(false);
expect(
normalizeExperimentalSettings({ enableExternalObjects: true }).enableDynamicSecrets,
).toBe(false);
});
it("round-trips an enableDynamicSecrets patch through the update merge", () => {
const current = normalizeExperimentalSettings({});
const enabled = normalizeExperimentalSettings({ ...current, enableDynamicSecrets: true });
expect(enabled.enableDynamicSecrets).toBe(true);
expect(enabled.enableStreamlinedLeftNavigation).toBe(true);
const disabled = normalizeExperimentalSettings({ ...enabled, enableDynamicSecrets: false });
expect(disabled).toEqual(current);
});
it("round-trips an enableConferenceRoomChat patch through the update merge", () => {
// updateExperimental merges `{ ...normalize(current), ...patch }` and
// re-normalizes; emulate that to prove the flag survives the roundtrip
+2
View File
@@ -53,6 +53,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableTaskWatchdogs: parsed.data.enableTaskWatchdogs ?? false,
enableCloudSync: parsed.data.enableCloudSync ?? false,
enableExternalObjects: parsed.data.enableExternalObjects ?? false,
enableDynamicSecrets: parsed.data.enableDynamicSecrets ?? false,
autoRestartDevServerWhenIdle: parsed.data.autoRestartDevServerWhenIdle ?? false,
enableIssueGraphLivenessAutoRecovery: parsed.data.enableIssueGraphLivenessAutoRecovery ?? false,
issueGraphLivenessAutoRecoveryLookbackHours:
@@ -70,6 +71,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableExperimentalFileViewer: false,
enableCloudSync: false,
enableExternalObjects: false,
enableDynamicSecrets: false,
autoRestartDevServerWhenIdle: false,
enableIssueGraphLivenessAutoRecovery: false,
issueGraphLivenessAutoRecoveryLookbackHours:
@@ -44,6 +44,8 @@ const STREAMLINED_TOGGLE_SELECTOR =
'button[aria-label="Toggle streamlined left navigation experimental setting"]';
const TASK_WATCHDOGS_TOGGLE_SELECTOR =
'button[aria-label="Toggle task watchdogs experimental setting"]';
const DYNAMIC_SECRETS_TOGGLE_SELECTOR =
'button[aria-label="Toggle dynamic secrets experimental setting"]';
function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload {
return {
@@ -54,6 +56,7 @@ function defaultExperimentalSettings(): InstanceExperimentalSettingsPayload {
enableIssuePlanDecompositions: false,
enableExperimentalFileViewer: false,
enableExternalObjects: false,
enableDynamicSecrets: false,
enableTaskWatchdogs: false,
enableCloudSync: false,
autoRestartDevServerWhenIdle: false,
@@ -192,4 +195,45 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233)
enableTaskWatchdogs: false,
});
});
it("renders and patches the Dynamic Secrets experimental toggle on and off", async () => {
await renderPage();
expect(container.textContent).toContain("Dynamic Secrets");
expect(container.textContent).toContain(
"Enables experimental dynamic secret resolution for later-stage secret bindings and command-backed values.",
);
const toggle = container.querySelector<HTMLButtonElement>(DYNAMIC_SECRETS_TOGGLE_SELECTOR);
expect(toggle?.getAttribute("aria-checked")).toBe("false");
await act(async () => {
toggle?.click();
});
await flushReact();
expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenCalledWith({
enableDynamicSecrets: true,
});
expect(toggle?.getAttribute("aria-checked")).toBe("true");
flushSync(() => {
root?.unmount();
});
root = null;
container.textContent = "";
await renderPage();
const enabledToggle = container.querySelector<HTMLButtonElement>(DYNAMIC_SECRETS_TOGGLE_SELECTOR);
expect(enabledToggle?.getAttribute("aria-checked")).toBe("true");
await act(async () => {
enabledToggle?.click();
});
await flushReact();
expect(mockInstanceSettingsApi.updateExperimental).toHaveBeenLastCalledWith({
enableDynamicSecrets: false,
});
});
});
@@ -244,6 +244,7 @@ export function InstanceExperimentalSettings() {
const enableTaskWatchdogs = experimentalQuery.data?.enableTaskWatchdogs === true;
const enableCloudSync = experimentalQuery.data?.enableCloudSync === true;
const enableExternalObjects = experimentalQuery.data?.enableExternalObjects === true;
const enableDynamicSecrets = experimentalQuery.data?.enableDynamicSecrets === true;
const autoRestartDevServerWhenIdle = experimentalQuery.data?.autoRestartDevServerWhenIdle === true;
const enableIssueGraphLivenessAutoRecovery =
experimentalQuery.data?.enableIssueGraphLivenessAutoRecovery === true;
@@ -335,6 +336,23 @@ export function InstanceExperimentalSettings() {
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Dynamic Secrets</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
Enables experimental dynamic secret resolution for later-stage secret bindings and command-backed values.
</p>
</div>
<ToggleSwitch
checked={enableDynamicSecrets}
onCheckedChange={(checked) => toggleMutation.mutate({ enableDynamicSecrets: checked })}
disabled={toggleMutation.isPending}
aria-label="Toggle dynamic secrets experimental setting"
/>
</div>
</section>
<section className="rounded-xl border border-border bg-card p-5">
<div className="flex items-start justify-between gap-4">
<div className="space-y-1.5">