mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-09 03:20:56 +00:00
refactor(api): align run projection types
This commit is contained in:
parent
3e97cae0ae
commit
6a8329b376
13 changed files with 383 additions and 6 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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", &[]),
|
||||
|
|
|
|||
|
|
@ -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::*;
|
||||
|
|
|
|||
31
lib/crates/fabro-api/tests/interview_option_round_trip.rs
Normal file
31
lib/crates/fabro-api/tests/interview_option_round_trip.rs
Normal file
|
|
@ -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::<ApiInterviewOption, InterviewOption>();
|
||||
}
|
||||
|
||||
#[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<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -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::<ApiInterviewQuestionRecord, InterviewQuestionRecord>();
|
||||
}
|
||||
|
||||
#[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<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -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::<ApiPendingInterviewRecord, PendingInterviewRecord>();
|
||||
}
|
||||
|
||||
#[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<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
114
lib/crates/fabro-api/tests/run_projection_round_trip.rs
Normal file
114
lib/crates/fabro-api/tests/run_projection_round_trip.rs
Normal file
|
|
@ -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::<ApiRunProjection, RunProjection>();
|
||||
}
|
||||
|
||||
#[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<T: 'static, U: 'static>() {
|
||||
assert_eq!(
|
||||
TypeId::of::<T>(),
|
||||
TypeId::of::<U>(),
|
||||
"{} should be the same type as {}",
|
||||
type_name::<T>(),
|
||||
type_name::<U>()
|
||||
);
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
24
lib/packages/fabro-api-client/src/models/interview-option.ts
Normal file
24
lib/packages/fabro-api-client/src/models/interview-option.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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<InterviewOption>;
|
||||
'allow_freeform': boolean;
|
||||
'timeout_seconds'?: number | null;
|
||||
'context_display'?: string | null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]`.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue