diff --git a/src/daytona/runtime.test.ts b/src/daytona/runtime.test.ts index 1dd26af..f0ebe08 100644 --- a/src/daytona/runtime.test.ts +++ b/src/daytona/runtime.test.ts @@ -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); diff --git a/src/daytona/runtime.ts b/src/daytona/runtime.ts index fc78edb..c597042 100644 --- a/src/daytona/runtime.ts +++ b/src/daytona/runtime.ts @@ -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 }).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; + }; + 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 diff --git a/src/index.ts b/src/index.ts index 9eb6976..8b5a473 100644 --- a/src/index.ts +++ b/src/index.ts @@ -30,6 +30,7 @@ export type { LifetimeMode, OutputStreamMode, SnapshotMode, + ResolvedSandboxRuntimeCapabilities, RunScriptResult, SandboxCapabilityModes, SandboxCountOptions, diff --git a/src/port.test.ts b/src/port.test.ts index 9ec3c50..3408ad7 100644 --- a/src/port.test.ts +++ b/src/port.test.ts @@ -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. */ @@ -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"); + }, + ); }); diff --git a/src/port.ts b/src/port.ts index c943136..9cd0f23 100644 --- a/src/port.ts +++ b/src/port.ts @@ -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 = { /** @@ -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; }; @@ -267,7 +288,7 @@ export type DeclaredSandboxRuntimeCapabilities = Pick< const capabilitiesByRuntime = new WeakMap< SandboxRuntime, - SandboxRuntimeCapabilities + ResolvedSandboxRuntimeCapabilities >(); /** @@ -282,7 +303,7 @@ const capabilitiesByRuntime = new WeakMap< */ export function resolveSandboxRuntimeCapabilities( runtime: SandboxRuntime, -): SandboxRuntimeCapabilities { +): ResolvedSandboxRuntimeCapabilities { const cached = capabilitiesByRuntime.get(runtime); if (cached) { return cached; @@ -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",