const std = @import("std"); const debug_trace = @import("../core/shared/debug_trace.zig");const devbox_executor = @import("../core/execution/devbox_executor.zig");const io_mod = @import("../core/shared/io.zig"); const Allocator = std.mem.Allocator;const CommandResult = devbox_executor.CommandResult;const Control = devbox_executor.Control;const VercelOutcome = devbox_executor.VercelOutcome; const vercel_http_timeout_sec: i64 = 30; pub const provider = devbox_executor.Provider{ .execute_fn = executeProvider }; fn executeProvider( _: ?*anyopaque, alloc: Allocator, command: []const u8, cwd: []const u8, control: Control,) devbox_executor.ProviderError!VercelOutcome { return executeVercel(alloc, command, cwd, control) catch |err| switch (err) { error.OutOfMemory => error.OutOfMemory, error.Cancelled => error.Cancelled, error.TimeoutExpired => error.TimeoutExpired, };} pub fn executeVercel( alloc: Allocator, command: []const u8, cwd: []const u8, control: Control,) !VercelOutcome { try control.check(); const token = getVercelAuthToken() orelse return .unavailable; if (try executeVercelControlled(alloc, command, cwd, token, control)) |result| { return .{ .success = result }; } return .request_failed;} fn setVercelHttpTimeout(conn: *std.http.Client.Connection) void { const sock = conn.stream_writer.stream.socket.handle; const timeout = std.posix.timeval{ .sec = vercel_http_timeout_sec, .usec = 0 }; std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, std.mem.asBytes(&timeout)) catch |err| { debug_trace.logf("core", "vercel sandbox receive timeout setup failed err={s}", .{@errorName(err)}); }; std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.SNDTIMEO, std.mem.asBytes(&timeout)) catch |err| { debug_trace.logf("core", "vercel sandbox send timeout setup failed err={s}", .{@errorName(err)}); };} fn executeVercelHttp(alloc: Allocator, scratch: Allocator, token: []const u8, command: []const u8, cwd: []const u8) !CommandResult { const started_ms = io_mod.milliTimestamp(); var body_writer: std.Io.Writer.Allocating = .init(scratch); defer body_writer.deinit(); try body_writer.writer.writeAll("{\"command\":\"sh\",\"args\":[\"-lc\","); try std.json.Stringify.value(command, .{}, &body_writer.writer); try body_writer.writer.writeAll("],\"cwd\":"); try std.json.Stringify.value(cwd, .{}, &body_writer.writer); try body_writer.writer.writeAll(",\"wait\":true}"); const request_body = try body_writer.toOwnedSlice(); defer scratch.free(request_body); const auth_header = try std.fmt.allocPrint(scratch, "Bearer {s}", .{token}); defer scratch.free(auth_header); var response_buf: std.Io.Writer.Allocating = .init(scratch); defer response_buf.deinit(); var client: std.http.Client = .{ .allocator = scratch, .io = io_mod.getIo() }; defer client.deinit(); const uri = try std.Uri.parse("https://api.vercel.com/v1/sandboxes/cmd"); var req = try client.request(.POST, uri, .{ .headers = .{ .authorization = .{ .override = auth_header }, .content_type = .{ .override = "application/json" }, }, .keep_alive = false, .redirect_behavior = .unhandled, }); defer req.deinit(); if (req.connection) |conn| setVercelHttpTimeout(conn); req.transfer_encoding = .{ .content_length = request_body.len }; var send_buf: [8192]u8 = undefined; var request_writer = try req.sendBodyUnflushed(&send_buf); try request_writer.writer.writeAll(request_body); try request_writer.end(); try req.connection.?.flush(); var response = try req.receiveHead(&.{}); if (response.head.status != .ok) { if (req.connection) |conn| conn.closing = true; return error.VercelResponseFailure; } const decompress_buffer: []u8 = switch (response.head.content_encoding) { .identity => &.{}, .zstd => try scratch.alloc(u8, std.compress.zstd.default_window_len), .deflate, .gzip => try scratch.alloc(u8, std.compress.flate.max_window_len), .compress => return error.UnsupportedCompressionMethod, }; defer if (decompress_buffer.len > 0) scratch.free(decompress_buffer); var transfer_buf: [64]u8 = undefined; var decompress: std.http.Decompress = undefined; const response_reader = response.readerDecompressing(&transfer_buf, &decompress, decompress_buffer); _ = response_reader.streamRemaining(&response_buf.writer) catch |err| switch (err) { error.ReadFailed => { if (response.bodyErr()) |body_err| return body_err; return err; }, else => |e| return e, }; const response_body = try response_buf.toOwnedSlice(); defer scratch.free(response_body); return parseVercelResponse(alloc, scratch, response_body, elapsedMs(started_ms, io_mod.milliTimestamp()));} const VercelHttpResultTag = enum { success, fallback, oom,}; const VercelHttpRequest = struct { command: []u8, cwd: []u8, token: []u8, fn create(command: []const u8, cwd: []const u8, token: []const u8) !*VercelHttpRequest { const alloc = std.heap.c_allocator; const self = try alloc.create(VercelHttpRequest); errdefer alloc.destroy(self); self.* = .{ .command = try alloc.dupe(u8, command), .cwd = &.{}, .token = &.{}, }; errdefer alloc.free(self.command); self.cwd = try alloc.dupe(u8, cwd); errdefer alloc.free(self.cwd); self.token = try alloc.dupe(u8, token); return self; } fn deinit(self: *VercelHttpRequest) void { const alloc = std.heap.c_allocator; alloc.free(self.command); alloc.free(self.cwd); alloc.free(self.token); alloc.destroy(self); }}; fn executeVercelControlled(alloc: Allocator, command: []const u8, cwd: []const u8, token: []const u8, control: Control) !?CommandResult { const request = try VercelHttpRequest.create(command, cwd, token); defer request.deinit(); if (!control.canInterrupt()) { const result = runVercelRequest(request) catch |err| switch (err) { error.OutOfMemory => return error.OutOfMemory, error.Canceled => return error.Cancelled, else => { debug_trace.logf("core", "vercel sandbox request failed err={s}", .{@errorName(err)}); return null; }, }; return finishVercelResult(alloc, result); } const zio = io_mod.getIo(); var queue_buf: [2]VercelRaceEvent = undefined; var queue = VercelRaceQueue.init(&queue_buf); var request_future = zio.concurrent(runVercelRequestQueued, .{ &queue, request }) catch |err| { debug_trace.logf("core", "vercel sandbox request concurrency unavailable err={s}", .{@errorName(err)}); return null; }; var control_future = zio.async(runVercelControlQueued, .{ &queue, control }); const event = queue.getOne(zio) catch |err| { request_future.cancel(zio); _ = control_future.cancel(zio) catch {}; debug_trace.logf("core", "vercel sandbox request race failed err={s}", .{@errorName(err)}); return null; }; switch (event) { .request => |result| { _ = control_future.cancel(zio) catch {}; request_future.await(zio); return finishVercelResult(alloc, result); }, .cancelled => { request_future.cancel(zio); _ = control_future.await(zio) catch {}; discardQueuedVercelEvents(&queue); return error.Cancelled; }, .timed_out => { request_future.cancel(zio); _ = control_future.await(zio) catch {}; discardQueuedVercelEvents(&queue); return error.TimeoutExpired; }, }} fn finishVercelResult(alloc: Allocator, result: VercelHttpResult) !?CommandResult { return switch (result) { .success => |output| blk: { var owned = output; defer owned.deinit(std.heap.c_allocator); break :blk try owned.clone(alloc); }, .fallback => null, .oom => error.OutOfMemory, };} fn runVercelRequest(request: *VercelHttpRequest) anyerror!VercelHttpResult { var scratch_state = std.heap.ArenaAllocator.init(std.heap.c_allocator); defer scratch_state.deinit(); const result = try executeVercelHttp(std.heap.c_allocator, scratch_state.allocator(), request.token, request.command, request.cwd); return .{ .success = result };} const VercelHttpResult = union(VercelHttpResultTag) { success: CommandResult, fallback, oom,}; const VercelRaceEvent = union(enum) { request: VercelHttpResult, cancelled, timed_out,}; const VercelRaceQueue = std.Io.Queue(VercelRaceEvent); fn runVercelRequestQueued(queue: *VercelRaceQueue, request: *VercelHttpRequest) void { const result = runVercelRequest(request) catch |err| switch (err) { error.Canceled => return, error.OutOfMemory => VercelHttpResult.oom, else => blk: { debug_trace.logf("core", "vercel sandbox request failed err={s}", .{@errorName(err)}); break :blk VercelHttpResult.fallback; }, }; queue.putOneUncancelable(io_mod.getIo(), .{ .request = result }) catch { freeVercelResult(result); };} fn runVercelControlQueued(queue: *VercelRaceQueue, control: Control) std.Io.Cancelable!void { while (true) { control.check() catch |err| switch (err) { error.Cancelled => { queue.putOneUncancelable(io_mod.getIo(), .cancelled) catch {}; return; }, error.TimeoutExpired => { queue.putOneUncancelable(io_mod.getIo(), .timed_out) catch {}; return; }, else => return, }; try std.Io.Timeout.sleep(.{ .duration = .{ .raw = .{ .nanoseconds = 100 * std.time.ns_per_ms }, .clock = .awake } }, io_mod.getIo()); }} fn discardQueuedVercelEvents(queue: *VercelRaceQueue) void { var events: [2]VercelRaceEvent = undefined; const count = queue.get(io_mod.getIo(), &events, 0) catch 0; for (events[0..count]) |event| { switch (event) { .request => |result| freeVercelResult(result), .cancelled, .timed_out => {}, } }} fn freeVercelResult(result: VercelHttpResult) void { switch (result) { .success => |output| { var owned = output; owned.deinit(std.heap.c_allocator); }, .fallback, .oom => {}, }} fn parseVercelResponse(alloc: Allocator, scratch: Allocator, body: []const u8, duration_ms: ?u64) !CommandResult { var parsed = std.json.parseFromSlice(std.json.Value, scratch, body, .{}) catch return error.VercelParseError; defer parsed.deinit(); if (parsed.value != .object) return error.VercelParseError; const exit_code_val = parsed.value.object.get("exitCode") orelse return error.VercelParseError; const stdout_val = parsed.value.object.get("stdout") orelse return error.VercelParseError; const stderr_val = parsed.value.object.get("stderr") orelse return error.VercelParseError; if (exit_code_val != .integer or stdout_val != .string or stderr_val != .string) return error.VercelParseError; const stdout = try alloc.dupe(u8, stdout_val.string); errdefer alloc.free(stdout); const stderr = try alloc.dupe(u8, stderr_val.string); errdefer alloc.free(stderr); return .{ .exit_code = exit_code_val.integer, .stdout = stdout, .stderr = stderr, .duration_ms = duration_ms, };} fn elapsedMs(started_ms: i64, finished_ms: i64) u64 { return if (finished_ms > started_ms) @intCast(finished_ms - started_ms) else 0;} fn getVercelAuthToken() ?[]const u8 { return getVercelAuthTokenForProbe(.{ .oidc_token = io_mod.getenv("VERCEL_OIDC_TOKEN"), .token = io_mod.getenv("VERCEL_TOKEN"), });} const VercelCredentialProbe = struct { oidc_token: ?[]const u8 = null, token: ?[]const u8 = null,}; fn getVercelAuthTokenForProbe(probe: VercelCredentialProbe) ?[]const u8 { if (probe.oidc_token) |token| return token; return probe.token;} test "devbox vercel control checks cancellation and timeout before remote work" { var cancel = std.atomic.Value(bool).init(true); try std.testing.expectError(error.Cancelled, executeVercel( std.testing.allocator, "printf no", "/tmp", .{ .cancel_flag = &cancel, .timeout_ms = 1000, .started_ms = io_mod.milliTimestamp(), }, )); try std.testing.expectError(error.TimeoutExpired, executeVercel( std.testing.allocator, "printf no", "/tmp", .{ .timeout_ms = 0, .started_ms = io_mod.milliTimestamp(), }, ));} test "devbox vercel auth token prefers oidc and falls back to token" { try std.testing.expectEqualStrings("oidc", getVercelAuthTokenForProbe(.{ .oidc_token = "oidc", .token = "token", }).?); try std.testing.expectEqualStrings("token", getVercelAuthTokenForProbe(.{ .token = "token" }).?); try std.testing.expect(getVercelAuthTokenForProbe(.{}) == null);} test "devbox vercel json parser handles success and parse failure" { var result = try parseVercelResponse( std.testing.allocator, std.testing.allocator, "{\"exitCode\":0,\"stdout\":\"ok\",\"stderr\":\"\"}", 7, ); defer result.deinit(std.testing.allocator); try std.testing.expectEqual(@as(i64, 0), result.exit_code); try std.testing.expectEqualStrings("ok", result.stdout); try std.testing.expectEqualStrings("", result.stderr); try std.testing.expectEqual(@as(?u64, 7), result.duration_ms); try std.testing.expectError( error.VercelParseError, parseVercelResponse(std.testing.allocator, std.testing.allocator, "{}", null), );}
const std = @import("std");
const debug_trace = @import("../core/shared/debug_trace.zig");
const devbox_executor = @import("../core/execution/devbox_executor.zig");
const io_mod = @import("../core/shared/io.zig");
const Allocator = std.mem.Allocator;
const CommandResult = devbox_executor.CommandResult;
const Control = devbox_executor.Control;
const VercelOutcome = devbox_executor.VercelOutcome;
const vercel_http_timeout_sec: i64 = 30;
pub const provider = devbox_executor.Provider{ .execute_fn = executeProvider };
fn executeProvider(
_: ?*anyopaque,
alloc: Allocator,
command: []const u8,
cwd: []const u8,
control: Control,
) devbox_executor.ProviderError!VercelOutcome {
return executeVercel(alloc, command, cwd, control) catch |err| switch (err) {
error.OutOfMemory => error.OutOfMemory,
error.Cancelled => error.Cancelled,
error.TimeoutExpired => error.TimeoutExpired,
};
}
pub fn executeVercel(
alloc: Allocator,
command: []const u8,
cwd: []const u8,
control: Control,
) !VercelOutcome {
try control.check();
const token = getVercelAuthToken() orelse return .unavailable;
if (try executeVercelControlled(alloc, command, cwd, token, control)) |result| {
return .{ .success = result };
}
return .request_failed;
}
fn setVercelHttpTimeout(conn: *std.http.Client.Connection) void {
const sock = conn.stream_writer.stream.socket.handle;
const timeout = std.posix.timeval{ .sec = vercel_http_timeout_sec, .usec = 0 };
std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.RCVTIMEO, std.mem.asBytes(&timeout)) catch |err| {
debug_trace.logf("core", "vercel sandbox receive timeout setup failed err={s}", .{@errorName(err)});
};
std.posix.setsockopt(sock, std.posix.SOL.SOCKET, std.posix.SO.SNDTIMEO, std.mem.asBytes(&timeout)) catch |err| {
debug_trace.logf("core", "vercel sandbox send timeout setup failed err={s}", .{@errorName(err)});
};
}
fn executeVercelHttp(alloc: Allocator, scratch: Allocator, token: []const u8, command: []const u8, cwd: []const u8) !CommandResult {
const started_ms = io_mod.milliTimestamp();
var body_writer: std.Io.Writer.Allocating = .init(scratch);
defer body_writer.deinit();
try body_writer.writer.writeAll("{\"command\":\"sh\",\"args\":[\"-lc\",");
try std.json.Stringify.value(command, .{}, &body_writer.writer);
try body_writer.writer.writeAll("],\"cwd\":");
try std.json.Stringify.value(cwd, .{}, &body_writer.writer);
try body_writer.writer.writeAll(",\"wait\":true}");
const request_body = try body_writer.toOwnedSlice();
defer scratch.free(request_body);
const auth_header = try std.fmt.allocPrint(scratch, "Bearer {s}", .{token});
defer scratch.free(auth_header);
var response_buf: std.Io.Writer.Allocating = .init(scratch);
defer response_buf.deinit();
var client: std.http.Client = .{ .allocator = scratch, .io = io_mod.getIo() };
defer client.deinit();
const uri = try std.Uri.parse("https://api.vercel.com/v1/sandboxes/cmd");
var req = try client.request(.POST, uri, .{
.headers = .{
.authorization = .{ .override = auth_header },
.content_type = .{ .override = "application/json" },
},
.keep_alive = false,
.redirect_behavior = .unhandled,
});
defer req.deinit();
if (req.connection) |conn| setVercelHttpTimeout(conn);
req.transfer_encoding = .{ .content_length = request_body.len };
var send_buf: [8192]u8 = undefined;
var request_writer = try req.sendBodyUnflushed(&send_buf);
try request_writer.writer.writeAll(request_body);
try request_writer.end();
try req.connection.?.flush();
var response = try req.receiveHead(&.{});
if (response.head.status != .ok) {
if (req.connection) |conn| conn.closing = true;
return error.VercelResponseFailure;
}
const decompress_buffer: []u8 = switch (response.head.content_encoding) {
.identity => &.{},
.zstd => try scratch.alloc(u8, std.compress.zstd.default_window_len),
.deflate, .gzip => try scratch.alloc(u8, std.compress.flate.max_window_len),
.compress => return error.UnsupportedCompressionMethod,
};
defer if (decompress_buffer.len > 0) scratch.free(decompress_buffer);
var transfer_buf: [64]u8 = undefined;
var decompress: std.http.Decompress = undefined;
const response_reader = response.readerDecompressing(&transfer_buf, &decompress, decompress_buffer);
_ = response_reader.streamRemaining(&response_buf.writer) catch |err| switch (err) {
error.ReadFailed => {
if (response.bodyErr()) |body_err| return body_err;
return err;
},
else => |e| return e,
};
const response_body = try response_buf.toOwnedSlice();
defer scratch.free(response_body);
return parseVercelResponse(alloc, scratch, response_body, elapsedMs(started_ms, io_mod.milliTimestamp()));
}
const VercelHttpResultTag = enum {
success,
fallback,
oom,
};
const VercelHttpRequest = struct {
command: []u8,
cwd: []u8,
token: []u8,
fn create(command: []const u8, cwd: []const u8, token: []const u8) !*VercelHttpRequest {
const alloc = std.heap.c_allocator;
const self = try alloc.create(VercelHttpRequest);
errdefer alloc.destroy(self);
self.* = .{
.command = try alloc.dupe(u8, command),
.cwd = &.{},
.token = &.{},
};
errdefer alloc.free(self.command);
self.cwd = try alloc.dupe(u8, cwd);
errdefer alloc.free(self.cwd);
self.token = try alloc.dupe(u8, token);
return self;
}
fn deinit(self: *VercelHttpRequest) void {
const alloc = std.heap.c_allocator;
alloc.free(self.command);
alloc.free(self.cwd);
alloc.free(self.token);
alloc.destroy(self);
}
};
fn executeVercelControlled(alloc: Allocator, command: []const u8, cwd: []const u8, token: []const u8, control: Control) !?CommandResult {
const request = try VercelHttpRequest.create(command, cwd, token);
defer request.deinit();
if (!control.canInterrupt()) {
const result = runVercelRequest(request) catch |err| switch (err) {
error.OutOfMemory => return error.OutOfMemory,
error.Canceled => return error.Cancelled,
else => {
debug_trace.logf("core", "vercel sandbox request failed err={s}", .{@errorName(err)});
return null;
},
};
return finishVercelResult(alloc, result);
}
const zio = io_mod.getIo();
var queue_buf: [2]VercelRaceEvent = undefined;
var queue = VercelRaceQueue.init(&queue_buf);
var request_future = zio.concurrent(runVercelRequestQueued, .{ &queue, request }) catch |err| {
debug_trace.logf("core", "vercel sandbox request concurrency unavailable err={s}", .{@errorName(err)});
return null;
};
var control_future = zio.async(runVercelControlQueued, .{ &queue, control });
const event = queue.getOne(zio) catch |err| {
request_future.cancel(zio);
_ = control_future.cancel(zio) catch {};
debug_trace.logf("core", "vercel sandbox request race failed err={s}", .{@errorName(err)});
return null;
};
switch (event) {
.request => |result| {
_ = control_future.cancel(zio) catch {};
request_future.await(zio);
return finishVercelResult(alloc, result);
},
.cancelled => {
request_future.cancel(zio);
_ = control_future.await(zio) catch {};
discardQueuedVercelEvents(&queue);
return error.Cancelled;
},
.timed_out => {
request_future.cancel(zio);
_ = control_future.await(zio) catch {};
discardQueuedVercelEvents(&queue);
return error.TimeoutExpired;
},
}
}
fn finishVercelResult(alloc: Allocator, result: VercelHttpResult) !?CommandResult {
return switch (result) {
.success => |output| blk: {
var owned = output;
defer owned.deinit(std.heap.c_allocator);
break :blk try owned.clone(alloc);
},
.fallback => null,
.oom => error.OutOfMemory,
};
}
fn runVercelRequest(request: *VercelHttpRequest) anyerror!VercelHttpResult {
var scratch_state = std.heap.ArenaAllocator.init(std.heap.c_allocator);
defer scratch_state.deinit();
const result = try executeVercelHttp(std.heap.c_allocator, scratch_state.allocator(), request.token, request.command, request.cwd);
return .{ .success = result };
}
const VercelHttpResult = union(VercelHttpResultTag) {
success: CommandResult,
fallback,
oom,
};
const VercelRaceEvent = union(enum) {
request: VercelHttpResult,
cancelled,
timed_out,
};
const VercelRaceQueue = std.Io.Queue(VercelRaceEvent);
fn runVercelRequestQueued(queue: *VercelRaceQueue, request: *VercelHttpRequest) void {
const result = runVercelRequest(request) catch |err| switch (err) {
error.Canceled => return,
error.OutOfMemory => VercelHttpResult.oom,
else => blk: {
debug_trace.logf("core", "vercel sandbox request failed err={s}", .{@errorName(err)});
break :blk VercelHttpResult.fallback;
},
};
queue.putOneUncancelable(io_mod.getIo(), .{ .request = result }) catch {
freeVercelResult(result);
};
}
fn runVercelControlQueued(queue: *VercelRaceQueue, control: Control) std.Io.Cancelable!void {
while (true) {
control.check() catch |err| switch (err) {
error.Cancelled => {
queue.putOneUncancelable(io_mod.getIo(), .cancelled) catch {};
return;
},
error.TimeoutExpired => {
queue.putOneUncancelable(io_mod.getIo(), .timed_out) catch {};
return;
},
else => return,
};
try std.Io.Timeout.sleep(.{ .duration = .{ .raw = .{ .nanoseconds = 100 * std.time.ns_per_ms }, .clock = .awake } }, io_mod.getIo());
}
}
fn discardQueuedVercelEvents(queue: *VercelRaceQueue) void {
var events: [2]VercelRaceEvent = undefined;
const count = queue.get(io_mod.getIo(), &events, 0) catch 0;
for (events[0..count]) |event| {
switch (event) {
.request => |result| freeVercelResult(result),
.cancelled, .timed_out => {},
}
}
}
fn freeVercelResult(result: VercelHttpResult) void {
switch (result) {
.success => |output| {
var owned = output;
owned.deinit(std.heap.c_allocator);
},
.fallback, .oom => {},
}
}
fn parseVercelResponse(alloc: Allocator, scratch: Allocator, body: []const u8, duration_ms: ?u64) !CommandResult {
var parsed = std.json.parseFromSlice(std.json.Value, scratch, body, .{}) catch return error.VercelParseError;
defer parsed.deinit();
if (parsed.value != .object) return error.VercelParseError;
const exit_code_val = parsed.value.object.get("exitCode") orelse return error.VercelParseError;
const stdout_val = parsed.value.object.get("stdout") orelse return error.VercelParseError;
const stderr_val = parsed.value.object.get("stderr") orelse return error.VercelParseError;
if (exit_code_val != .integer or stdout_val != .string or stderr_val != .string) return error.VercelParseError;
const stdout = try alloc.dupe(u8, stdout_val.string);
errdefer alloc.free(stdout);
const stderr = try alloc.dupe(u8, stderr_val.string);
errdefer alloc.free(stderr);
return .{
.exit_code = exit_code_val.integer,
.stdout = stdout,
.stderr = stderr,
.duration_ms = duration_ms,
};
}
fn elapsedMs(started_ms: i64, finished_ms: i64) u64 {
return if (finished_ms > started_ms) @intCast(finished_ms - started_ms) else 0;
}
fn getVercelAuthToken() ?[]const u8 {
return getVercelAuthTokenForProbe(.{
.oidc_token = io_mod.getenv("VERCEL_OIDC_TOKEN"),
.token = io_mod.getenv("VERCEL_TOKEN"),
});
}
const VercelCredentialProbe = struct {
oidc_token: ?[]const u8 = null,
token: ?[]const u8 = null,
};
fn getVercelAuthTokenForProbe(probe: VercelCredentialProbe) ?[]const u8 {
if (probe.oidc_token) |token| return token;
return probe.token;
}
test "devbox vercel control checks cancellation and timeout before remote work" {
var cancel = std.atomic.Value(bool).init(true);
try std.testing.expectError(error.Cancelled, executeVercel(
std.testing.allocator,
"printf no",
"/tmp",
.{
.cancel_flag = &cancel,
.timeout_ms = 1000,
.started_ms = io_mod.milliTimestamp(),
},
));
try std.testing.expectError(error.TimeoutExpired, executeVercel(
std.testing.allocator,
"printf no",
"/tmp",
.{
.timeout_ms = 0,
.started_ms = io_mod.milliTimestamp(),
},
));
}
test "devbox vercel auth token prefers oidc and falls back to token" {
try std.testing.expectEqualStrings("oidc", getVercelAuthTokenForProbe(.{
.oidc_token = "oidc",
.token = "token",
}).?);
try std.testing.expectEqualStrings("token", getVercelAuthTokenForProbe(.{ .token = "token" }).?);
try std.testing.expect(getVercelAuthTokenForProbe(.{}) == null);
}
test "devbox vercel json parser handles success and parse failure" {
var result = try parseVercelResponse(
std.testing.allocator,
std.testing.allocator,
"{\"exitCode\":0,\"stdout\":\"ok\",\"stderr\":\"\"}",
7,
);
defer result.deinit(std.testing.allocator);
try std.testing.expectEqual(@as(i64, 0), result.exit_code);
try std.testing.expectEqualStrings("ok", result.stdout);
try std.testing.expectEqualStrings("", result.stderr);
try std.testing.expectEqual(@as(?u64, 7), result.duration_ms);
try std.testing.expectError(
error.VercelParseError,
parseVercelResponse(std.testing.allocator, std.testing.allocator, "{}", null),
);
}