Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/daytona/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1920,7 +1920,16 @@ describe('DaytonaRuntime smoke', { concurrency: false }, () => {
if (isTestDaytonaNotFound(error)) continue;
throw error;
}
await daytona.delete(sandbox);
try {
await daytona.delete(sandbox);
} catch (deleteError) {
// Daytona get/delete is eventually consistent: get can still resolve
// for a sandbox that runtime.destroy() already removed, and the
// subsequent delete then rejects 404. That case is cleanup success,
// not failure — assertDaytonaSandboxGone below confirms absence
// regardless. Any other delete error is real and must still bubble.
if (!isTestDaytonaNotFound(deleteError)) throw deleteError;
}
await assertDaytonaSandboxGone(daytona, id);
} catch (error) {
cleanupFailures.push(error);
Expand Down
15 changes: 13 additions & 2 deletions src/daytona/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -778,8 +778,19 @@ export class DaytonaRuntime implements WorkflowRuntime {
// without ever going through get(), which leaves env, volumes, and
// network settings unpopulated until refreshData() runs. Hydrate before
// reading it for replacementCreateParams so those settings are not
// silently dropped from the replacement.
await (originalSandbox as unknown as { refreshData?: () => Promise<void> }).refreshData?.();
// silently dropped from the replacement. Bound the refresh with the same
// lookup deadline used elsewhere so a hanging SDK call cannot leave
// recreateAfterFailedStart wedged before the replacement is ever created.
const refreshableSandbox = originalSandbox as unknown as {
refreshData?: () => Promise<void>;
};
if (typeof refreshableSandbox.refreshData === 'function') {
await awaitLookupOperation(
Promise.resolve(refreshableSandbox.refreshData.call(originalSandbox)),
lookupDeadline(undefined),
`refreshing sandbox ${originalId} before replacement`,
);
}

try {
// Do not copy the name: Daytona requires names to be unique while the
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type {
LifetimeMode,
OutputStreamMode,
SnapshotMode,
ResolvedSandboxRuntimeCapabilities,
RunScriptResult,
SandboxCapabilityModes,
SandboxCountOptions,
Expand Down
32 changes: 32 additions & 0 deletions src/port.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { describe, it } from "node:test";
import {
isPendingEvidence,
resolveSandboxRuntimeCapabilities,
type ResolvedSandboxRuntimeCapabilities,
type SandboxRuntime,
type SandboxRuntimeCapabilities,
} from "./port.js";

/** Minimal runtime: only what the resolver actually inspects. */
Expand Down Expand Up @@ -119,4 +121,34 @@ describe("capability modes", () => {
resolveSandboxRuntimeCapabilities(instance),
);
});

it(
"lets a consumer literal-construct SandboxRuntimeCapabilities without modes",
() => {
// Source-compat contract: the exported base shape must still accept the
// five pre-modes fields alone. External TypeScript consumers that built
// fixtures like this before modes existed compile unchanged.
const preModesFixture: SandboxRuntimeCapabilities = {
asyncExec: false,
reattach: false,
detachedLaunch: false,
warmLease: true,
lifecycle: true,
};
assert.equal(preModesFixture.modes, undefined);
},
);

it(
"resolver returns the stricter ResolvedSandboxRuntimeCapabilities with modes populated",
() => {
// The resolver's return type has modes required. Assign into the strict
// type without a cast: the compiler enforces that modes is present, and
// the runtime confirms it's populated (with unknowns by default).
const resolved: ResolvedSandboxRuntimeCapabilities =
resolveSandboxRuntimeCapabilities(runtime());
assert.equal(resolved.modes.outputStreams, "unknown");
assert.equal(resolved.modes.filesystem, "unknown");
},
);
});
31 changes: 26 additions & 5 deletions src/port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,11 @@ export type SandboxRuntime = {
* narrow `RuntimeCapabilities` in `./types.ts`, which belongs to the live
* in-sandbox bootstrap plane and must not be conflated with it. The two are
* kept under distinct names on purpose.
*
* `modes` is optional on this shape so a TypeScript consumer can still
* literal-construct a fixture with the five booleans. The resolver returns the
* stricter `ResolvedSandboxRuntimeCapabilities` where `modes` is required and
* always populated (defaulting to `"unknown"` rather than to a claim).
*/
export type SandboxRuntimeCapabilities = {
/**
Expand All @@ -249,9 +254,25 @@ export type SandboxRuntimeCapabilities = {
/** `start`/`stop` actually change sandbox state rather than no-opping. */
readonly lifecycle: boolean;
/**
* Structured detail for the capabilities a boolean flattens. Always present
* after resolution, defaulting to `"unknown"` rather than to a claim.
* Structured detail for the capabilities a boolean flattens. Optional on the
* base shape for source-compat with pre-modes fixtures; always populated on
* the resolver's return type (`ResolvedSandboxRuntimeCapabilities`).
*/
readonly modes?: SandboxCapabilityModes;
};

/**
* The descriptor `resolveSandboxRuntimeCapabilities` returns. `modes` is
* required here — the resolver always populates it, defaulting to `"unknown"`
* so a runtime that declares nothing makes no new claim while still producing
* a fully-shaped resolved descriptor.
*
* Kept distinct from `SandboxRuntimeCapabilities` so external consumers that
* literal-construct fixtures with only the pre-modes fields continue to
* compile; those fixtures satisfy `SandboxRuntimeCapabilities`, and only code
* reading a resolver output relies on `modes` being present.
*/
export type ResolvedSandboxRuntimeCapabilities = SandboxRuntimeCapabilities & {
readonly modes: SandboxCapabilityModes;
};

Expand All @@ -267,7 +288,7 @@ export type DeclaredSandboxRuntimeCapabilities = Pick<

const capabilitiesByRuntime = new WeakMap<
SandboxRuntime,
SandboxRuntimeCapabilities
ResolvedSandboxRuntimeCapabilities
>();

/**
Expand All @@ -282,7 +303,7 @@ const capabilitiesByRuntime = new WeakMap<
*/
export function resolveSandboxRuntimeCapabilities(
runtime: SandboxRuntime,
): SandboxRuntimeCapabilities {
): ResolvedSandboxRuntimeCapabilities {
const cached = capabilitiesByRuntime.get(runtime);
if (cached) {
return cached;
Expand All @@ -295,7 +316,7 @@ export function resolveSandboxRuntimeCapabilities(
&& typeof runtime.getById === "function"
&& typeof runtime.getScriptStatus === "function"
&& typeof runtime.getScriptLogs === "function";
const resolved: SandboxRuntimeCapabilities = {
const resolved: ResolvedSandboxRuntimeCapabilities = {
asyncExec,
reattach: typeof runtime.getById === "function",
detachedLaunch: typeof runtime.launchDetached === "function",
Expand Down
Loading