diff --git a/internal/client/client.go b/internal/client/client.go index 4c610ec..434daf8 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1176,6 +1176,29 @@ func (c *Client) UpdatePrompt(ctx context.Context, slugOrID string, req UpdatePr return &response, nil } +// PromptVersionListEntry represents a single version in the versions list response. +type PromptVersionListEntry struct { + ID string `json:"id"` // prompt_version_id + PromptVersion int `json:"prompt_version"` +} + +// ListPromptVersions lists all versions of a prompt, sorted newest-first. +func (c *Client) ListPromptVersions(ctx context.Context, slugOrID string) ([]PromptVersionListEntry, error) { + respBody, err := c.doRequest(ctx, http.MethodGet, "/prompts/"+slugOrID+"/versions", nil) + if err != nil { + return nil, err + } + + var response struct { + Data []PromptVersionListEntry `json:"data"` + } + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling versions response: %w", err) + } + + return response.Data, nil +} + // MakePromptVersionDefault makes a specific version the default func (c *Client) MakePromptVersionDefault(ctx context.Context, slugOrID string, version int) error { req := map[string]int{"version": version} @@ -1307,6 +1330,29 @@ func (c *Client) UpdatePromptPartial(ctx context.Context, slugOrID string, req U return &response, nil } +// PromptPartialVersionListEntry represents a single version in the versions list response. +type PromptPartialVersionListEntry struct { + PromptPartialVersionID string `json:"prompt_partial_version_id"` + Version int `json:"version"` +} + +// ListPromptPartialVersions lists all versions of a prompt partial, sorted newest-first. +func (c *Client) ListPromptPartialVersions(ctx context.Context, slugOrID string) ([]PromptPartialVersionListEntry, error) { + respBody, err := c.doRequest(ctx, http.MethodGet, "/prompts/partials/"+slugOrID+"/versions", nil) + if err != nil { + return nil, err + } + + var response struct { + Data []PromptPartialVersionListEntry `json:"data"` + } + if err := json.Unmarshal(respBody, &response); err != nil { + return nil, fmt.Errorf("error unmarshaling versions response: %w", err) + } + + return response.Data, nil +} + // MakePromptPartialVersionDefault makes a specific version the default func (c *Client) MakePromptPartialVersionDefault(ctx context.Context, slugOrID string, version int) error { req := map[string]int{"version": version} diff --git a/internal/provider/map_state_test.go b/internal/provider/map_state_test.go new file mode 100644 index 0000000..5228935 --- /dev/null +++ b/internal/provider/map_state_test.go @@ -0,0 +1,331 @@ +package provider + +import ( + "testing" + "time" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/portkey-ai/terraform-provider-portkey/internal/client" +) + +// --- mapPartialToState tests --- + +func TestMapPartialToState_ExternalChangeDetected(t *testing.T) { + r := &promptPartialResource{} + + state := &promptPartialResourceModel{ + Content: types.StringValue("terraform content"), + Version: types.Int64Value(2), + PromptPartialVersionID: types.StringValue("old-version-id"), + } + + partial := &client.PromptPartial{ + ID: "id-1", + Slug: "my-partial", + Name: "My Partial", + String: "console-edited content", + Status: "active", + Version: 3, + PromptPartialVersionID: "new-version-id", + CreatedAt: time.Now(), + } + + r.mapPartialToState(state, partial) + + if state.Content.ValueString() != "console-edited content" { + t.Errorf("expected content to be refreshed from API, got %q", state.Content.ValueString()) + } + if state.Version.ValueInt64() != 3 { + t.Errorf("expected version 3, got %d", state.Version.ValueInt64()) + } + if state.PromptPartialVersionID.ValueString() != "new-version-id" { + t.Errorf("expected new version ID, got %q", state.PromptPartialVersionID.ValueString()) + } +} + +func TestMapPartialToState_NoExternalChange(t *testing.T) { + r := &promptPartialResource{} + + state := &promptPartialResourceModel{ + Content: types.StringValue("terraform content"), + Version: types.Int64Value(2), + PromptPartialVersionID: types.StringValue("current-version-id"), + } + + // API returns same version — no external change, content preserved from state + partial := &client.PromptPartial{ + ID: "id-1", + Slug: "my-partial", + Name: "My Partial", + String: "stale api content", + Status: "active", + Version: 2, + PromptPartialVersionID: "current-version-id", + CreatedAt: time.Now(), + } + + r.mapPartialToState(state, partial) + + if state.Content.ValueString() != "terraform content" { + t.Errorf("expected content to be preserved from state, got %q", state.Content.ValueString()) + } +} + +func TestMapPartialToState_RollbackDetected(t *testing.T) { + r := &promptPartialResource{} + + // State at version 3 + state := &promptPartialResourceModel{ + Content: types.StringValue("terraform content"), + Version: types.Int64Value(3), + PromptPartialVersionID: types.StringValue("version-id-3"), + } + + // API returns version 1 — someone rolled back in console + partial := &client.PromptPartial{ + ID: "id-1", + Slug: "my-partial", + Name: "My Partial", + String: "rolled-back content", + Status: "active", + Version: 1, + PromptPartialVersionID: "version-id-1", + CreatedAt: time.Now(), + } + + r.mapPartialToState(state, partial) + + if state.Content.ValueString() != "rolled-back content" { + t.Errorf("expected content to be refreshed from API on rollback, got %q", state.Content.ValueString()) + } + if state.Version.ValueInt64() != 1 { + t.Errorf("expected version 1, got %d", state.Version.ValueInt64()) + } +} + +func TestMapPartialToState_FirstPopulation(t *testing.T) { + r := &promptPartialResource{} + + // Fresh state (create or import) — version is null + state := &promptPartialResourceModel{} + + partial := &client.PromptPartial{ + ID: "id-1", + Slug: "my-partial", + Name: "My Partial", + String: "api content", + Status: "active", + Version: 1, + PromptPartialVersionID: "version-id-1", + CreatedAt: time.Now(), + } + + r.mapPartialToState(state, partial) + + if state.Content.ValueString() != "api content" { + t.Errorf("expected content from API on first population, got %q", state.Content.ValueString()) + } + if state.Version.ValueInt64() != 1 { + t.Errorf("expected version 1, got %d", state.Version.ValueInt64()) + } +} + +func TestMapPartialToState_VersionDescriptionNotImported(t *testing.T) { + r := &promptPartialResource{} + + // State has no version_description (user didn't set it) + state := &promptPartialResourceModel{ + Content: types.StringValue("terraform content"), + Version: types.Int64Value(1), + } + + // API returns a version_description from a console edit + partial := &client.PromptPartial{ + ID: "id-1", + Slug: "my-partial", + Name: "My Partial", + String: "terraform content", + Status: "active", + Version: 1, + VersionDescription: "set via console", + CreatedAt: time.Now(), + } + + r.mapPartialToState(state, partial) + + if !state.VersionDescription.IsNull() { + t.Errorf("expected version_description to remain null, got %q", state.VersionDescription.ValueString()) + } +} + +// --- mapPromptToState tests --- + +func TestMapPromptToState_ExternalChangeDetected(t *testing.T) { + r := &promptResource{} + + state := &promptResourceModel{ + Template: types.StringValue("terraform template"), + Model: types.StringValue("gpt-4"), + PromptVersion: types.Int64Value(1), + Parameters: types.StringValue(`{"model":"gpt-4"}`), + } + + // API returns version 2 — someone edited in console + prompt := &client.Prompt{ + ID: "id-1", + Slug: "my-prompt", + Name: "My Prompt", + String: "console-edited template", + Model: "gpt-5-mini", + Status: "active", + PromptVersion: 2, + PromptVersionID: "version-id-2", + PromptVersionStatus: "active", + CreatedAt: time.Now(), + } + + r.mapPromptToState(state, prompt) + + if state.Template.ValueString() != "console-edited template" { + t.Errorf("expected template to be refreshed from API, got %q", state.Template.ValueString()) + } + if state.Model.ValueString() != "gpt-5-mini" { + t.Errorf("expected model to be refreshed from API, got %q", state.Model.ValueString()) + } + if state.PromptVersion.ValueInt64() != 2 { + t.Errorf("expected version 2, got %d", state.PromptVersion.ValueInt64()) + } +} + +func TestMapPromptToState_NoExternalChange(t *testing.T) { + r := &promptResource{} + + state := &promptResourceModel{ + Template: types.StringValue("terraform template"), + Model: types.StringValue("gpt-4"), + PromptVersion: types.Int64Value(1), + PromptVersionID: types.StringValue("version-id-1"), + Parameters: types.StringValue(`{"model":"gpt-4"}`), + } + + // API returns same version — content preserved from state + prompt := &client.Prompt{ + ID: "id-1", + Slug: "my-prompt", + Name: "My Prompt", + String: "stale api template", + Model: "stale-model", + Status: "active", + PromptVersion: 1, + PromptVersionID: "version-id-1", + PromptVersionStatus: "active", + CreatedAt: time.Now(), + } + + r.mapPromptToState(state, prompt) + + if state.Template.ValueString() != "terraform template" { + t.Errorf("expected template to be preserved from state, got %q", state.Template.ValueString()) + } + if state.Model.ValueString() != "gpt-4" { + t.Errorf("expected model to be preserved from state, got %q", state.Model.ValueString()) + } +} + +func TestMapPromptToState_RollbackDetected(t *testing.T) { + r := &promptResource{} + + // State at version 3 + state := &promptResourceModel{ + Template: types.StringValue("terraform template"), + Model: types.StringValue("gpt-4"), + PromptVersion: types.Int64Value(3), + Parameters: types.StringValue(`{"model":"gpt-4"}`), + } + + // API returns version 1 — someone rolled back in console + prompt := &client.Prompt{ + ID: "id-1", + Slug: "my-prompt", + Name: "My Prompt", + String: "rolled-back template", + Model: "gpt-3.5-turbo", + Status: "active", + PromptVersion: 1, + PromptVersionID: "version-id-1", + PromptVersionStatus: "active", + CreatedAt: time.Now(), + } + + r.mapPromptToState(state, prompt) + + if state.Template.ValueString() != "rolled-back template" { + t.Errorf("expected template to be refreshed from API on rollback, got %q", state.Template.ValueString()) + } + if state.Model.ValueString() != "gpt-3.5-turbo" { + t.Errorf("expected model to be refreshed from API on rollback, got %q", state.Model.ValueString()) + } + if state.PromptVersion.ValueInt64() != 1 { + t.Errorf("expected version 1, got %d", state.PromptVersion.ValueInt64()) + } +} + +func TestMapPromptToState_FirstPopulation(t *testing.T) { + r := &promptResource{} + + // Fresh state (create or import) + state := &promptResourceModel{} + + prompt := &client.Prompt{ + ID: "id-1", + Slug: "my-prompt", + Name: "My Prompt", + String: "api template", + Model: "gpt-4", + Status: "active", + PromptVersion: 1, + PromptVersionID: "version-id-1", + PromptVersionStatus: "active", + CreatedAt: time.Now(), + } + + r.mapPromptToState(state, prompt) + + if state.Template.ValueString() != "api template" { + t.Errorf("expected template from API on first population, got %q", state.Template.ValueString()) + } + if state.PromptVersion.ValueInt64() != 1 { + t.Errorf("expected version 1, got %d", state.PromptVersion.ValueInt64()) + } +} + +func TestMapPromptToState_VersionDescriptionNotImported(t *testing.T) { + r := &promptResource{} + + state := &promptResourceModel{ + Template: types.StringValue("template"), + Model: types.StringValue("gpt-4"), + PromptVersion: types.Int64Value(1), + Parameters: types.StringValue(`{"model":"gpt-4"}`), + } + + prompt := &client.Prompt{ + ID: "id-1", + Slug: "my-prompt", + Name: "My Prompt", + String: "template", + Model: "gpt-4", + Status: "active", + PromptVersion: 1, + PromptVersionID: "version-id-1", + PromptVersionStatus: "active", + PromptVersionDescription: "set via console", + CreatedAt: time.Now(), + } + + r.mapPromptToState(state, prompt) + + if !state.VersionDescription.IsNull() { + t.Errorf("expected version_description to remain null, got %q", state.VersionDescription.ValueString()) + } +} diff --git a/internal/provider/prompt_partial_resource.go b/internal/provider/prompt_partial_resource.go index 0563c41..8cbf480 100644 --- a/internal/provider/prompt_partial_resource.go +++ b/internal/provider/prompt_partial_resource.go @@ -198,8 +198,8 @@ func (r *promptPartialResource) Read(ctx context.Context, req resource.ReadReque return } - // Fetch the partial from the API. Content/Version/VersionID are preserved - // from state by mapPartialToState to avoid eventual consistency issues. + // Fetch the partial from the API. mapPartialToState detects external + // changes by comparing versions and refreshes content if needed. partial, err := r.client.GetPromptPartial(ctx, state.Slug.ValueString(), "") if err != nil { resp.Diagnostics.AddError( @@ -268,6 +268,7 @@ func (r *promptPartialResource) Update(ctx context.Context, req resource.UpdateR // Only call update if there are changes var updateResp *client.UpdatePromptPartialResponse + var newVersion int if nameChanged || contentChanged { var err error updateResp, err = r.client.UpdatePromptPartial(ctx, state.Slug.ValueString(), updateReq) @@ -279,16 +280,36 @@ func (r *promptPartialResource) Update(ctx context.Context, req resource.UpdateR return } - // If a new version was created (content changed), make it the default + // If a new version was created (content changed), make it the default. + // Look up the real version number from the versions list by matching + // the version ID returned by Update, since the MakeDefault endpoint + // requires a version number (not a UUID). if contentChanged && updateResp.PromptPartialVersionID != "" { - // NOTE: This assumes versions increment by 1. If versions are created - // outside Terraform (UI, API, another workspace), the version in state - // may be stale, causing this to target the wrong version number. - // The Portkey API's makeDefault endpoint requires a version number, - // not a version ID, so we cannot use the returned version_id directly. - newVersion := int(state.Version.ValueInt64()) + 1 - - // Make the new version the default + versions, err := r.client.ListPromptPartialVersions(ctx, state.Slug.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error listing prompt partial versions", + "Could not list versions to find new version number: "+err.Error(), + ) + return + } + + newVersion = -1 + for _, v := range versions { + if v.PromptPartialVersionID == updateResp.PromptPartialVersionID { + newVersion = v.Version + break + } + } + + if newVersion == -1 { + resp.Diagnostics.AddError( + "Error finding new prompt partial version", + "Could not find version number for version ID "+updateResp.PromptPartialVersionID+" in versions list", + ) + return + } + err = r.client.MakePromptPartialVersionDefault(ctx, state.Slug.ValueString(), newVersion) if err != nil { resp.Diagnostics.AddError( @@ -304,13 +325,9 @@ func (r *promptPartialResource) Update(ctx context.Context, req resource.UpdateR // We trust the plan values for fields we sent, and derive computed fields. plan.ID = state.ID plan.Slug = state.Slug - // plan.Name already has correct value from plan - // plan.Content already has correct value from plan - // plan.WorkspaceID already has correct value from plan - // plan.VersionDescription already has correct value from plan - if contentChanged { - plan.Version = types.Int64Value(state.Version.ValueInt64() + 1) + if contentChanged && newVersion > 0 { + plan.Version = types.Int64Value(int64(newVersion)) plan.PromptPartialVersionID = types.StringValue(updateResp.PromptPartialVersionID) } else { plan.Version = state.Version @@ -361,33 +378,37 @@ func (r *promptPartialResource) ImportState(ctx context.Context, req resource.Im } // mapPartialToState maps a PromptPartial API response to the Terraform state model. -// Fields managed by Terraform (Content, Version, PromptPartialVersionID) are preserved -// from state when already set to avoid eventual consistency issues with the Portkey API. +// Detects external changes by comparing API version to state version. If the API +// version is higher, someone edited outside Terraform and we refresh from the API +// so Terraform can detect the drift and overwrite back to config values. func (r *promptPartialResource) mapPartialToState(state *promptPartialResourceModel, partial *client.PromptPartial) { state.ID = types.StringValue(partial.ID) state.Slug = types.StringValue(partial.Slug) state.Name = types.StringValue(partial.Name) state.Status = types.StringValue(partial.Status) - // Preserve Content, Version, and VersionID from state if already set. - // The Portkey API has eventual consistency — GET may return stale data - // after updates. We trust the values set during Create/Update. - if state.Content.IsNull() || state.Content.IsUnknown() { + // Detect external changes: if the API version differs from state, someone + // edited outside Terraform (new version or rollback). Refresh from API so + // Terraform sees the drift and overwrites back to config values on next apply. + externalChange := !state.Version.IsNull() && !state.Version.IsUnknown() && + int64(partial.Version) != state.Version.ValueInt64() + + if externalChange || state.Content.IsNull() || state.Content.IsUnknown() { state.Content = types.StringValue(partial.String) } - if state.Version.IsNull() || state.Version.IsUnknown() { - state.Version = types.Int64Value(int64(partial.Version)) - } - if state.PromptPartialVersionID.IsNull() || state.PromptPartialVersionID.IsUnknown() { - state.PromptPartialVersionID = types.StringValue(partial.PromptPartialVersionID) - } + + // Always update version and version ID from API — these are computed fields + // that should reflect reality. + state.Version = types.Int64Value(int64(partial.Version)) + state.PromptPartialVersionID = types.StringValue(partial.PromptPartialVersionID) // Preserve workspace_id from state — the API does not return it in the // PromptPartial response, so we must never overwrite the user-supplied value. - if partial.VersionDescription != "" { - state.VersionDescription = types.StringValue(partial.VersionDescription) - } + // Only preserve version_description from state — never import it from the API. + // The API may return a version_description set via console edits, but if the + // Terraform config doesn't set it, importing it would cause perpetual drift + // (state has value, config doesn't, plan always wants to null it out). state.CreatedAt = types.StringValue(partial.CreatedAt.Format("2006-01-02T15:04:05Z07:00")) if !partial.UpdatedAt.IsZero() { diff --git a/internal/provider/prompt_resource.go b/internal/provider/prompt_resource.go index d6014ec..60acf32 100644 --- a/internal/provider/prompt_resource.go +++ b/internal/provider/prompt_resource.go @@ -231,8 +231,8 @@ func (r *promptResource) Read(ctx context.Context, req resource.ReadRequest, res return } - // Fetch the prompt from the API. Template/Version/VersionID are preserved - // from state by mapPromptToState to avoid eventual consistency issues. + // Fetch the prompt from the API. mapPromptToState detects external + // changes by comparing versions and refreshes content if needed. prompt, err := r.client.GetPrompt(ctx, state.Slug.ValueString(), "") if err != nil { resp.Diagnostics.AddError( @@ -347,6 +347,7 @@ func (r *promptResource) Update(ctx context.Context, req resource.UpdateRequest, // Only call update if there are changes var updateResp *client.UpdatePromptResponse + var newVersion int if nameChanged || versionUpdateRequired || virtualKeyChanged { var err error updateResp, err = r.client.UpdatePrompt(ctx, state.Slug.ValueString(), updateReq) @@ -358,16 +359,36 @@ func (r *promptResource) Update(ctx context.Context, req resource.UpdateRequest, return } - // If a new version was created, make it the default + // If a new version was created, make it the default. + // Look up the real version number from the versions list by matching + // the version ID returned by Update, since the MakeDefault endpoint + // requires a version number (not a UUID). if versionUpdateRequired && updateResp.PromptVersionID != "" { - // NOTE: This assumes versions increment by 1. If versions are created - // outside Terraform (UI, API, another workspace), the version in state - // may be stale, causing this to target the wrong version number. - // The Portkey API's makeDefault endpoint requires a version number, - // not a version ID, so we cannot use the returned version_id directly. - newVersion := int(state.PromptVersion.ValueInt64()) + 1 - - // Make the new version the default + versions, err := r.client.ListPromptVersions(ctx, state.Slug.ValueString()) + if err != nil { + resp.Diagnostics.AddError( + "Error listing prompt versions", + "Could not list versions to find new version number: "+err.Error(), + ) + return + } + + newVersion = -1 + for _, v := range versions { + if v.ID == updateResp.PromptVersionID { + newVersion = v.PromptVersion + break + } + } + + if newVersion == -1 { + resp.Diagnostics.AddError( + "Error finding new prompt version", + "Could not find version number for version ID "+updateResp.PromptVersionID+" in versions list", + ) + return + } + err = r.client.MakePromptVersionDefault(ctx, state.Slug.ValueString(), newVersion) if err != nil { resp.Diagnostics.AddError( @@ -391,8 +412,8 @@ func (r *promptResource) Update(ctx context.Context, req resource.UpdateRequest, plan.Parameters = state.Parameters } - if versionUpdateRequired { - plan.PromptVersion = types.Int64Value(state.PromptVersion.ValueInt64() + 1) + if versionUpdateRequired && newVersion > 0 { + plan.PromptVersion = types.Int64Value(int64(newVersion)) plan.PromptVersionID = types.StringValue(updateResp.PromptVersionID) } else { plan.PromptVersion = state.PromptVersion @@ -437,9 +458,10 @@ func (r *promptResource) ImportState(ctx context.Context, req resource.ImportSta resource.ImportStatePassthroughID(ctx, path.Root("slug"), req, resp) } -// mapPromptToState maps a Prompt API response to the Terraform state model -// preserveUserValues should be true when we want to keep the user-provided values -// that may differ from what the API returns (e.g., IDs vs slugs) +// mapPromptToState maps a Prompt API response to the Terraform state model. +// Detects external changes by comparing API version to state version. If the API +// version differs, someone edited outside Terraform and we refresh from the API +// so Terraform can detect the drift and overwrite back to config values. func (r *promptResource) mapPromptToState(state *promptResourceModel, prompt *client.Prompt) { state.ID = types.StringValue(prompt.ID) state.Slug = types.StringValue(prompt.Slug) @@ -448,29 +470,33 @@ func (r *promptResource) mapPromptToState(state *promptResourceModel, prompt *cl if state.CollectionID.IsNull() || state.CollectionID.IsUnknown() { state.CollectionID = types.StringValue(prompt.CollectionID) } - // Preserve Template, Model, VirtualKey, PromptVersion, and PromptVersionID - // from state when already set. The Portkey API has eventual consistency — - // GET may return stale data after updates. We trust Create/Update values. - if state.Template.IsNull() || state.Template.IsUnknown() { + // Detect external changes: if the API version differs from state, someone + // edited outside Terraform (new version or rollback). Refresh from API so + // Terraform sees the drift and overwrites back to config values on next apply. + externalChange := !state.PromptVersion.IsNull() && !state.PromptVersion.IsUnknown() && + int64(prompt.PromptVersion) != state.PromptVersion.ValueInt64() + + if externalChange || state.Template.IsNull() || state.Template.IsUnknown() { state.Template = types.StringValue(prompt.String) } - if state.Model.IsNull() || state.Model.IsUnknown() { + if externalChange || state.Model.IsNull() || state.Model.IsUnknown() { state.Model = types.StringValue(prompt.Model) } + // Keep the user-provided virtual_key value (API returns slug but user may have provided ID) if state.VirtualKey.IsNull() || state.VirtualKey.IsUnknown() { state.VirtualKey = types.StringValue(prompt.VirtualKey) } - if state.PromptVersion.IsNull() || state.PromptVersion.IsUnknown() { - state.PromptVersion = types.Int64Value(int64(prompt.PromptVersion)) - } - if state.PromptVersionID.IsNull() || state.PromptVersionID.IsUnknown() { - state.PromptVersionID = types.StringValue(prompt.PromptVersionID) - } + + // Always update version and version ID from API — these are computed fields + // that should reflect reality. + state.PromptVersion = types.Int64Value(int64(prompt.PromptVersion)) + state.PromptVersionID = types.StringValue(prompt.PromptVersionID) state.PromptVersionStatus = types.StringValue(prompt.PromptVersionStatus) state.Status = types.StringValue(prompt.Status) - // Keep user's parameters if set - API may add additional fields like "model" - if state.Parameters.IsNull() || state.Parameters.IsUnknown() { + // Refresh parameters on external change or first population. + // Preserved from state otherwise since API may add extra fields like "model". + if externalChange || state.Parameters.IsNull() || state.Parameters.IsUnknown() { if prompt.Parameters != nil { paramsBytes, err := json.Marshal(prompt.Parameters) if err == nil { @@ -481,9 +507,10 @@ func (r *promptResource) mapPromptToState(state *promptResourceModel, prompt *cl } } - if prompt.PromptVersionDescription != "" { - state.VersionDescription = types.StringValue(prompt.PromptVersionDescription) - } + // Only preserve version_description from state — never import it from the API. + // The API may return a version_description set via console edits, but if the + // Terraform config doesn't set it, importing it would cause perpetual drift + // (state has value, config doesn't, plan always wants to null it out). state.CreatedAt = types.StringValue(prompt.CreatedAt.Format("2006-01-02T15:04:05Z07:00")) if !prompt.UpdatedAt.IsZero() {