|
| 1 | +import fs from 'node:fs'; |
1 | 2 | import path from 'node:path'; |
| 3 | +import type { CliFlags } from '@agent-device/contracts/command'; |
2 | 4 | import type { AgentDeviceClient } from '../agent-device-client.ts'; |
3 | 5 | import { |
4 | 6 | hashRemoteConfigFile, |
@@ -158,3 +160,123 @@ export function seedConnectionState(options: { |
158 | 160 | writeRemoteConnectionState({ stateDir: options.stateDir, state }); |
159 | 161 | return state; |
160 | 162 | } |
| 163 | + |
| 164 | +export type LeaseAllocateRequest = Parameters<AgentDeviceClient['leases']['allocate']>[0]; |
| 165 | +export type LeaseReleaseRequest = Parameters<AgentDeviceClient['leases']['release']>[0]; |
| 166 | + |
| 167 | +/** A `leases.release` stub that reports success and keeps the last request it received. */ |
| 168 | +export function recordedLeaseRelease(): { |
| 169 | + stub: AgentDeviceClient['leases']['release']; |
| 170 | + readonly request: LeaseReleaseRequest | undefined; |
| 171 | +} { |
| 172 | + let request: LeaseReleaseRequest | undefined; |
| 173 | + return { |
| 174 | + stub: async (incoming) => { |
| 175 | + request = incoming; |
| 176 | + return { released: true }; |
| 177 | + }, |
| 178 | + get request() { |
| 179 | + return request; |
| 180 | + }, |
| 181 | + }; |
| 182 | +} |
| 183 | + |
| 184 | +/** |
| 185 | + * A `leases.allocate` stub that grants `leaseId` under the request's own scope and provider |
| 186 | + * fields, and keeps the last request it received. |
| 187 | + */ |
| 188 | +export function recordedLeaseAllocate(options: { |
| 189 | + leaseId: string; |
| 190 | + backend: NonNullable<LeaseAllocateRequest['leaseBackend']>; |
| 191 | +}): { |
| 192 | + stub: AgentDeviceClient['leases']['allocate']; |
| 193 | + readonly request: LeaseAllocateRequest | undefined; |
| 194 | +} { |
| 195 | + let request: LeaseAllocateRequest | undefined; |
| 196 | + return { |
| 197 | + stub: async (incoming) => { |
| 198 | + request = incoming; |
| 199 | + return { |
| 200 | + leaseId: options.leaseId, |
| 201 | + tenantId: incoming.tenant, |
| 202 | + runId: incoming.runId, |
| 203 | + backend: incoming.leaseBackend ?? options.backend, |
| 204 | + leaseProvider: incoming.leaseProvider, |
| 205 | + clientId: incoming.clientId, |
| 206 | + deviceKey: incoming.deviceKey, |
| 207 | + }; |
| 208 | + }, |
| 209 | + get request() { |
| 210 | + return request; |
| 211 | + }, |
| 212 | + }; |
| 213 | +} |
| 214 | + |
| 215 | +export type ReplacedProfiles = { |
| 216 | + oldRemoteConfigPath: string; |
| 217 | + newRemoteConfigPath: string; |
| 218 | +}; |
| 219 | + |
| 220 | +/** |
| 221 | + * Two profile files in one workspace: the previous connection's, describing https://old.example |
| 222 | + * with `previousToken` when given, and the one `connect --force` moves to, describing |
| 223 | + * https://new.example. |
| 224 | + */ |
| 225 | +export function writeReplacedProfiles( |
| 226 | + tempRoot: string, |
| 227 | + options: { previousToken?: string } = {}, |
| 228 | +): ReplacedProfiles { |
| 229 | + const oldRemoteConfigPath = path.join(tempRoot, 'old-remote.json'); |
| 230 | + const newRemoteConfigPath = path.join(tempRoot, 'new-remote.json'); |
| 231 | + fs.writeFileSync( |
| 232 | + oldRemoteConfigPath, |
| 233 | + JSON.stringify({ |
| 234 | + daemonBaseUrl: 'https://old.example', |
| 235 | + ...(options.previousToken ? { daemonAuthToken: options.previousToken } : {}), |
| 236 | + }), |
| 237 | + ); |
| 238 | + fs.writeFileSync(newRemoteConfigPath, JSON.stringify({ daemonBaseUrl: 'https://new.example' })); |
| 239 | + return { oldRemoteConfigPath, newRemoteConfigPath }; |
| 240 | +} |
| 241 | + |
| 242 | +/** |
| 243 | + * Persists the connection a `connect --force` replaces: session `adc-android` holding lease |
| 244 | + * `lease-old` under run `run-old` against https://old.example. |
| 245 | + */ |
| 246 | +export function seedPreviousConnection(options: { |
| 247 | + stateDir: string; |
| 248 | + remoteConfigPath: string; |
| 249 | + overrides?: Partial<StoredConnectionSeed>; |
| 250 | +}): RemoteConnectionState { |
| 251 | + return seedConnectionState({ |
| 252 | + stateDir: options.stateDir, |
| 253 | + state: { |
| 254 | + session: 'adc-android', |
| 255 | + remoteConfigPath: options.remoteConfigPath, |
| 256 | + tenant: 'acme', |
| 257 | + runId: 'run-old', |
| 258 | + leaseId: 'lease-old', |
| 259 | + leaseBackend: 'android-instance', |
| 260 | + daemon: { baseUrl: 'https://old.example' }, |
| 261 | + ...options.overrides, |
| 262 | + }, |
| 263 | + }); |
| 264 | +} |
| 265 | + |
| 266 | +/** `connect --force` flags that move session `adc-android` to run `run-new` at https://new.example. */ |
| 267 | +export function forceConnectFlags( |
| 268 | + options: { stateDir: string; remoteConfig: string } & Partial<CliFlags>, |
| 269 | +): CliFlags { |
| 270 | + return { |
| 271 | + json: true, |
| 272 | + help: false, |
| 273 | + version: false, |
| 274 | + force: true, |
| 275 | + daemonBaseUrl: 'https://new.example', |
| 276 | + tenant: 'acme', |
| 277 | + runId: 'run-new', |
| 278 | + session: 'adc-android', |
| 279 | + platform: 'android', |
| 280 | + ...options, |
| 281 | + }; |
| 282 | +} |
0 commit comments