mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Add dynamic secrets experimental flag
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
7 files changed
+111
No files matched your search
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in new issue
Block a user