diff --git a/docs/public/api-reference/fabro-api.yaml b/docs/public/api-reference/fabro-api.yaml index a3a2ff8aa..d6a39bb3b 100644 --- a/docs/public/api-reference/fabro-api.yaml +++ b/docs/public/api-reference/fabro-api.yaml @@ -4946,12 +4946,56 @@ components: stderr: type: ["string", "null"] + InterviewOption: + description: Option stored with an interview question in the event log. + type: object + required: + - key + - label + properties: + key: + type: string + label: + type: string + + InterviewQuestionRecord: + description: Storage shape of an interview question recorded in the event log. + type: object + required: + - id + - text + - stage + - question_type + - allow_freeform + properties: + id: + type: string + text: + type: string + stage: + type: string + question_type: + $ref: "#/components/schemas/QuestionType" + options: + type: array + items: + $ref: "#/components/schemas/InterviewOption" + allow_freeform: + type: boolean + timeout_seconds: + type: ["number", "null"] + format: double + context_display: + type: ["string", "null"] + PendingInterviewRecord: description: Pending interview question plus the time it entered the unresolved set. type: object + required: + - question properties: question: - $ref: "#/components/schemas/ApiQuestion" + $ref: "#/components/schemas/InterviewQuestionRecord" started_at: type: ["string", "null"] format: date-time @@ -5041,6 +5085,10 @@ components: - type: string format: date-time - type: "null" + pending_control: + oneOf: + - $ref: "#/components/schemas/RunControlAction" + - type: "null" checkpoint: oneOf: - $ref: "#/components/schemas/RunCheckpoint" diff --git a/lib/crates/fabro-api/build.rs b/lib/crates/fabro-api/build.rs index 005afdc40..ca7e62f75 100644 --- a/lib/crates/fabro-api/build.rs +++ b/lib/crates/fabro-api/build.rs @@ -316,6 +316,18 @@ fn main() { ("InternalStageStatus", "fabro_types::StageStatus", &[]), ("NodeState", "fabro_types::NodeState", &[]), ("SecretMetadata", "fabro_types::SecretMetadata", &[]), + ("InterviewOption", "fabro_types::InterviewOption", &[]), + ( + "InterviewQuestionRecord", + "fabro_types::InterviewQuestionRecord", + &[], + ), + ( + "PendingInterviewRecord", + "fabro_types::PendingInterviewRecord", + &[], + ), + ("RunProjection", "fabro_types::RunProjection", &[]), ("PullRequestRecord", "fabro_types::PullRequestRecord", &[]), ("PullRequestDetail", "fabro_types::PullRequestDetail", &[]), ("PullRequestUser", "fabro_types::PullRequestUser", &[]), diff --git a/lib/crates/fabro-api/src/lib.rs b/lib/crates/fabro-api/src/lib.rs index f535ed56c..3c1c507d4 100644 --- a/lib/crates/fabro-api/src/lib.rs +++ b/lib/crates/fabro-api/src/lib.rs @@ -28,9 +28,11 @@ pub mod types { BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason, TerminalStatus, }; pub use fabro_types::{ - ActorKind, ActorRef, DiffStats, DirtyStatus, GitContext, NodeState, NodeStatusRecord, - PreRunPushOutcome, QuestionType, RepositoryReference, RunSummary, SecretMetadata, - SecretType, ServerSettings, StageStatus as InternalStageStatus, WorkflowSettings, + ActorKind, ActorRef, DiffStats, DirtyStatus, GitContext, InterviewOption, + InterviewQuestionRecord, NodeState, NodeStatusRecord, PendingInterviewRecord, + PreRunPushOutcome, QuestionType, RepositoryReference, RunProjection, RunSummary, + SecretMetadata, SecretType, ServerSettings, StageStatus as InternalStageStatus, + WorkflowSettings, }; pub use crate::generated::types::*; diff --git a/lib/crates/fabro-api/tests/interview_option_round_trip.rs b/lib/crates/fabro-api/tests/interview_option_round_trip.rs new file mode 100644 index 000000000..40fb51b23 --- /dev/null +++ b/lib/crates/fabro-api/tests/interview_option_round_trip.rs @@ -0,0 +1,31 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::InterviewOption as ApiInterviewOption; +use fabro_types::InterviewOption; +use serde_json::json; + +#[test] +fn interview_option_reuses_canonical_type() { + assert_same_type::(); +} + +#[test] +fn interview_option_round_trips_representative_json() { + let value = json!({ + "key": "approve", + "label": "Approve" + }); + + let option: InterviewOption = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(option).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-api/tests/interview_question_record_round_trip.rs b/lib/crates/fabro-api/tests/interview_question_record_round_trip.rs new file mode 100644 index 000000000..8e8f743b6 --- /dev/null +++ b/lib/crates/fabro-api/tests/interview_question_record_round_trip.rs @@ -0,0 +1,40 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::InterviewQuestionRecord as ApiInterviewQuestionRecord; +use fabro_types::InterviewQuestionRecord; +use serde_json::json; + +#[test] +fn interview_question_record_reuses_canonical_type() { + assert_same_type::(); +} + +#[test] +fn interview_question_record_round_trips_representative_json() { + let value = json!({ + "id": "q-1", + "text": "Approve deploy?", + "stage": "gate", + "question_type": "multiple_choice", + "options": [ + { "key": "approve", "label": "Approve" }, + { "key": "reject", "label": "Reject" } + ], + "allow_freeform": true, + "timeout_seconds": 30.0, + "context_display": "Diff summary" + }); + + let question: InterviewQuestionRecord = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(question).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-api/tests/pending_interview_record_round_trip.rs b/lib/crates/fabro-api/tests/pending_interview_record_round_trip.rs new file mode 100644 index 000000000..ed6d53707 --- /dev/null +++ b/lib/crates/fabro-api/tests/pending_interview_record_round_trip.rs @@ -0,0 +1,60 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::PendingInterviewRecord as ApiPendingInterviewRecord; +use fabro_types::PendingInterviewRecord; +use serde_json::json; + +#[test] +fn pending_interview_record_reuses_canonical_type() { + assert_same_type::(); +} + +#[test] +fn pending_interview_record_round_trips_populated_question() { + let value = json!({ + "question": { + "id": "q-1", + "text": "Approve deploy?", + "stage": "gate", + "question_type": "multiple_choice", + "options": [ + { "key": "approve", "label": "Approve" }, + { "key": "reject", "label": "Reject" } + ], + "allow_freeform": true, + "timeout_seconds": 30.0, + "context_display": "Diff summary" + }, + "started_at": "2026-04-29T12:34:56Z" + }); + + let record: PendingInterviewRecord = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(record).unwrap(), value); +} + +#[test] +fn pending_interview_record_allows_empty_options_to_be_omitted() { + let value = json!({ + "question": { + "id": "q-2", + "text": "Any notes?", + "stage": "notes", + "question_type": "freeform", + "allow_freeform": true + }, + "started_at": null + }); + + let record: PendingInterviewRecord = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(record).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/crates/fabro-api/tests/run_projection_round_trip.rs b/lib/crates/fabro-api/tests/run_projection_round_trip.rs new file mode 100644 index 000000000..20c6bac2d --- /dev/null +++ b/lib/crates/fabro-api/tests/run_projection_round_trip.rs @@ -0,0 +1,114 @@ +use std::any::{TypeId, type_name}; + +use fabro_api::types::RunProjection as ApiRunProjection; +use fabro_types::RunProjection; +use serde_json::json; + +#[test] +fn run_projection_reuses_canonical_type() { + assert_same_type::(); +} + +#[test] +fn run_projection_round_trips_populated_projection() { + let value = json!({ + "spec": null, + "graph_source": null, + "start": null, + "status": null, + "status_updated_at": null, + "pending_control": "pause", + "checkpoint": null, + "checkpoints": [[ + 7, + { + "timestamp": "2026-04-29T12:34:56Z", + "current_node": "build", + "completed_nodes": ["build"], + "node_retries": {}, + "context_values": {}, + "node_visits": { "build": 2 } + } + ]], + "conclusion": null, + "retro": null, + "retro_prompt": null, + "retro_response": null, + "sandbox": null, + "final_patch": null, + "pull_request": null, + "superseded_by": null, + "pending_interviews": { + "q-1": { + "question": { + "id": "q-1", + "text": "Approve deploy?", + "stage": "gate", + "question_type": "multiple_choice", + "options": [ + { "key": "approve", "label": "Approve" }, + { "key": "reject", "label": "Reject" } + ], + "allow_freeform": true, + "timeout_seconds": 30.0, + "context_display": "Diff summary" + }, + "started_at": "2026-04-29T12:35:00Z" + } + }, + "nodes": { + "build@2": { + "prompt": null, + "response": null, + "status": null, + "provider_used": null, + "diff": "diff --git a/file b/file", + "script_invocation": null, + "script_timing": null, + "parallel_results": null, + "stdout": "done", + "stderr": null + } + } + }); + + let projection: RunProjection = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(projection).unwrap(), value); +} + +#[test] +fn run_projection_round_trips_with_pending_control_unset() { + let value = json!({ + "spec": null, + "graph_source": null, + "start": null, + "status": null, + "status_updated_at": null, + "pending_control": null, + "checkpoint": null, + "checkpoints": [], + "conclusion": null, + "retro": null, + "retro_prompt": null, + "retro_response": null, + "sandbox": null, + "final_patch": null, + "pull_request": null, + "superseded_by": null, + "pending_interviews": {}, + "nodes": {} + }); + + let projection: RunProjection = serde_json::from_value(value.clone()).unwrap(); + assert_eq!(serde_json::to_value(projection).unwrap(), value); +} + +fn assert_same_type() { + assert_eq!( + TypeId::of::(), + TypeId::of::(), + "{} should be the same type as {}", + type_name::(), + type_name::() + ); +} diff --git a/lib/packages/fabro-api-client/src/.openapi-generator/FILES b/lib/packages/fabro-api-client/src/.openapi-generator/FILES index 4f159dd97..808623739 100644 --- a/lib/packages/fabro-api-client/src/.openapi-generator/FILES +++ b/lib/packages/fabro-api-client/src/.openapi-generator/FILES @@ -107,6 +107,8 @@ models/install-server-config-input.ts models/install-session-response.ts models/integration-webhooks-settings.ts models/internal-stage-status.ts +models/interview-option.ts +models/interview-question-record.ts models/ip-allow-entry.ts models/literal-ip-allow-entry.ts models/log-destination.ts diff --git a/lib/packages/fabro-api-client/src/models/index.ts b/lib/packages/fabro-api-client/src/models/index.ts index ea1e2d74f..504a11ba2 100644 --- a/lib/packages/fabro-api-client/src/models/index.ts +++ b/lib/packages/fabro-api-client/src/models/index.ts @@ -86,6 +86,8 @@ export * from './install-server-config-input'; export * from './install-session-response'; export * from './integration-webhooks-settings'; export * from './internal-stage-status'; +export * from './interview-option'; +export * from './interview-question-record'; export * from './ip-allow-entry'; export * from './literal-ip-allow-entry'; export * from './log-destination'; diff --git a/lib/packages/fabro-api-client/src/models/interview-option.ts b/lib/packages/fabro-api-client/src/models/interview-option.ts new file mode 100644 index 000000000..b5e546395 --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/interview-option.ts @@ -0,0 +1,24 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * Option stored with an interview question in the event log. + */ +export interface InterviewOption { + 'key': string; + 'label': string; +} + diff --git a/lib/packages/fabro-api-client/src/models/interview-question-record.ts b/lib/packages/fabro-api-client/src/models/interview-question-record.ts new file mode 100644 index 000000000..971f143bc --- /dev/null +++ b/lib/packages/fabro-api-client/src/models/interview-question-record.ts @@ -0,0 +1,38 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Fabro Run API + * HTTP API for managing Fabro workflow run executions. + * + * The version of the OpenAPI document: 0.1.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { InterviewOption } from './interview-option'; +// May contain unused imports in some cases +// @ts-ignore +import type { QuestionType } from './question-type'; + +/** + * Storage shape of an interview question recorded in the event log. + */ +export interface InterviewQuestionRecord { + 'id': string; + 'text': string; + 'stage': string; + 'question_type': QuestionType; + 'options'?: Array; + 'allow_freeform': boolean; + 'timeout_seconds'?: number | null; + 'context_display'?: string | null; +} + + + diff --git a/lib/packages/fabro-api-client/src/models/pending-interview-record.ts b/lib/packages/fabro-api-client/src/models/pending-interview-record.ts index ea925d33d..54c663934 100644 --- a/lib/packages/fabro-api-client/src/models/pending-interview-record.ts +++ b/lib/packages/fabro-api-client/src/models/pending-interview-record.ts @@ -15,13 +15,13 @@ // May contain unused imports in some cases // @ts-ignore -import type { ApiQuestion } from './api-question'; +import type { InterviewQuestionRecord } from './interview-question-record'; /** * Pending interview question plus the time it entered the unresolved set. */ export interface PendingInterviewRecord { - 'question'?: ApiQuestion; + 'question': InterviewQuestionRecord; 'started_at'?: string | null; } diff --git a/lib/packages/fabro-api-client/src/models/run-projection.ts b/lib/packages/fabro-api-client/src/models/run-projection.ts index de92af9bf..49acc2fec 100644 --- a/lib/packages/fabro-api-client/src/models/run-projection.ts +++ b/lib/packages/fabro-api-client/src/models/run-projection.ts @@ -24,6 +24,9 @@ import type { PendingInterviewRecord } from './pending-interview-record'; import type { RunCheckpoint } from './run-checkpoint'; // May contain unused imports in some cases // @ts-ignore +import type { RunControlAction } from './run-control-action'; +// May contain unused imports in some cases +// @ts-ignore import type { RunProjectionCheckpointsInnerInner } from './run-projection-checkpoints-inner-inner'; // May contain unused imports in some cases // @ts-ignore @@ -41,6 +44,7 @@ export interface RunProjection { 'start'?: { [key: string]: any; } | null; 'status'?: RunStatus | null; 'status_updated_at'?: string | null; + 'pending_control'?: RunControlAction | null; 'checkpoint'?: RunCheckpoint | null; /** * Sequence-tagged checkpoint history entries as `[seq, checkpoint]`.