qcode/ui/profiles/
status.rs1use crate::profile::{Profile, SafeName};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum Readiness {
13 Unknown,
15 Present,
17 Missing,
19}
20
21impl Readiness {
22 #[must_use]
24 pub fn of(present: bool) -> Self {
25 if present { Self::Present } else { Self::Missing }
26 }
27}
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Revision {
32 Unknown,
34 Current,
36 Earlier,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct Status {
45 pub name: SafeName,
47 pub image: Readiness,
49 pub revision: Revision,
51 pub identity: Readiness,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Row {
58 pub profile: Profile,
60 pub image: Readiness,
62 pub revision: Revision,
64 pub identity: Readiness,
66}
67
68impl Row {
69 #[must_use]
71 pub fn new(profile: Profile) -> Self {
72 Self { profile, image: Readiness::Unknown, revision: Revision::Unknown, identity: Readiness::Unknown }
73 }
74
75 #[must_use]
78 pub fn is_runnable(&self) -> bool {
79 self.image == Readiness::Present && (self.is_signed_in() || !self.profile.account.needs_login())
80 }
81
82 #[must_use]
84 pub fn is_signed_in(&self) -> bool {
85 self.identity == Readiness::Present
86 }
87
88 pub fn apply(&mut self, status: &Status) {
90 if status.name == self.profile.name {
91 self.image = status.image;
92 self.revision = status.revision;
93 self.identity = status.identity;
94 }
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::profile::{AccountKind, HarnessKind, MountAccess, NetworkMode, Template};
102
103 fn profile(name: &str) -> Profile {
104 Profile {
105 name: SafeName::parse(name).expect("the name is safe"),
106 harness: HarnessKind::ClaudeCode,
107 template: Template::Recommended,
108 account: AccountKind::Subscription,
109 provider: None,
110 assets: MountAccess::ReadOnly,
111 network: NetworkMode::Full,
112 without: Vec::new(),
113 os: crate::base::Os::Debian,
114 }
115 }
116
117 #[test]
118 fn nothing_is_ready_before_the_engine_has_answered() {
119 let row = Row::new(profile("claude-sub"));
120 assert_eq!(row.image, Readiness::Unknown);
121 assert_eq!(row.identity, Readiness::Unknown);
122 assert!(!row.is_runnable(), "an unanswered profile is never treated as ready");
123 }
124
125 #[test]
126 fn a_profile_is_runnable_only_with_both_an_image_and_a_login() {
127 let mut row = Row::new(profile("claude-sub"));
128 row.apply(&Status {
129 name: SafeName::parse("claude-sub").expect("safe"),
130 image: Readiness::Present,
131 revision: Revision::Current,
132 identity: Readiness::Missing,
133 });
134 assert!(!row.is_runnable());
135 row.apply(&Status {
136 name: SafeName::parse("claude-sub").expect("safe"),
137 image: Readiness::Present,
138 revision: Revision::Current,
139 identity: Readiness::Present,
140 });
141 assert!(row.is_runnable());
142 }
143
144 #[test]
145 fn a_free_profile_is_runnable_with_its_image_alone() {
146 let mut free = profile("oc");
147 free.harness = HarnessKind::OpenCode;
148 free.account = AccountKind::Free;
149 let mut row = Row::new(free);
150 assert!(!row.is_runnable(), "an image nobody has checked is still not a yes");
151 row.apply(&Status {
152 name: SafeName::parse("oc").expect("safe"),
153 image: Readiness::Present,
154 revision: Revision::Current,
155 identity: Readiness::Missing,
156 });
157 assert!(row.is_runnable(), "no login is kept, and none is needed");
158 }
159
160 #[test]
161 fn an_answer_about_another_profile_is_left_alone() {
162 let mut row = Row::new(profile("claude-sub"));
163 row.apply(&Status {
164 name: SafeName::parse("codex-key").expect("safe"),
165 image: Readiness::Present,
166 revision: Revision::Current,
167 identity: Readiness::Present,
168 });
169 assert_eq!(row.image, Readiness::Unknown);
170 assert_eq!(row.identity, Readiness::Unknown);
171 }
172}