Skip to content
Merged
7 changes: 6 additions & 1 deletion docs/sandbox-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,10 @@ that fact and exits successfully. To start one, use `winapp run . --on sandbox -
The report distinguishes what the guest supports from what the current client can do;
a minimized client can prevent input or capture even when the guest supports both.
Use the guest window list for UI PIDs, not a deployment's tracked launcher process.
The JSON `workRoot` field (shown as `Work root` in text output) is the absolute base
for relative file-transfer paths, normally `C:\WinApp\work`. It is separate from
`capabilities.managedRoot`, normally `C:\WinApp`, and is omitted when the guest
does not report its managed root.
If several client windows prevent an unambiguous capture, the error lists candidates;
decide which to close before retrying.

Expand All @@ -284,7 +288,8 @@ standard streams, and returns the command's exit code. It is not a full interact
terminal; console applications see redirected pipes. `--json` formats winapp errors,
not the child command's stdout.

For `push` and `pull`, **target paths are relative to `C:\WinApp\work`**. Absolute,
For `push` and `pull`, **target paths are relative to the `workRoot` reported by
[`target snapshot`](#inspecting-the-sandbox)**. Absolute,
rooted, and UNC target paths are refused. A single file lands at exactly the destination
you name; a directory preserves its structure beneath that destination. Use the
resolved guest path printed after a push as the next command's `--cwd`.
Expand Down
59 changes: 58 additions & 1 deletion src/winapp-CLI/WinApp.Cli.Tests/TargetCaptureCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,61 @@ public void Cleanup()

// ---- snapshot ------------------------------------------------------------------

[TestMethod]
public async Task Snapshot_WorkRootMatchesRelativeTransfersUnderTheReportedNondefaultRoot()
{
await using var harness = new Harness(GuestWindows());
var console = new TestConsole();
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
var files = new GuestFileService(harness.GuestManaged);
var workRoot = files.ResolveScopeDirectory(TargetFileTransferService.WorkScope, create: false);
Comment thread
nmetulev marked this conversation as resolved.

Assert.AreEqual(0, await RunSnapshotAsync(harness, console, "sandbox", "--json"));

using var document = JsonDocument.Parse(console.Output);
Assert.AreEqual(harness.GuestManaged,
document.RootElement.GetProperty("capabilities").GetProperty("managedRoot").GetString());
Assert.IsTrue(document.RootElement.TryGetProperty("workRoot", out var reported),
"Snapshot must expose the working directory separately from managedRoot.");
Assert.AreEqual(workRoot, reported.GetString());
Assert.IsFalse(Directory.Exists(workRoot), "Reporting the work root must not create it.");
Assert.AreEqual(0, harness.Backend.EnsureCalls);
Assert.AreEqual(0, harness.Rendering.ResolveSurfaceCalls);
}

[TestMethod]
public async Task Snapshot_HumanOutputReportsWorkRoot()
{
await using var harness = new Harness(GuestWindows());
var console = new TestConsole();
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
console.Profile.Width = 240;

Assert.AreEqual(0, await RunSnapshotAsync(harness, console, "sandbox"));

StringAssert.Contains(console.Output, "Work root: " +
new GuestFileService(harness.GuestManaged)
.ResolveScopeDirectory(TargetFileTransferService.WorkScope, create: false));
}

[TestMethod]
[DataRow(false, false, false)]
[DataRow(true, false, true)]
[DataRow(true, true, false)]
public async Task Snapshot_UnavailableRootDoesNotInventWorkRoot(
bool running, bool agentAnswers, bool reportsManagedRoot)
{
await using var harness = new Harness(GuestWindows());
harness.Backend.Running = running;
harness.Backend.AgentAnswers = agentAnswers;
harness.Backend.ReportsManagedRoot = reportsManagedRoot;
var console = new TestConsole();
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed

Assert.AreEqual(0, await RunSnapshotAsync(harness, console, "sandbox", "--json"));

using var document = JsonDocument.Parse(console.Output);
Assert.IsFalse(document.RootElement.TryGetProperty("workRoot", out _));
Assert.AreEqual(0, harness.Backend.EnsureCalls);
}

[TestMethod]
public async Task Snapshot_ReportsReadinessTheDesktopWindowAndWhatIsOnIt()
{
Expand Down Expand Up @@ -1160,6 +1215,8 @@ private class FakeBackend(
/// <summary>Whether the running target's agent answers an inspect-only attach.</summary>
public bool AgentAnswers { get; set; } = true;

public bool ReportsManagedRoot { get; set; } = true;

/// <summary>The host end of the last channel handed out, so a test can watch it close.</summary>
public IGuestTransport? LastHostTransport { get; private set; }

Expand Down Expand Up @@ -1201,7 +1258,7 @@ private TargetConnection Connect()
new ScriptedGuestWinapp(harness, stdout, exitCode, request => Requests.Add(request)),
new StaticGuestSessionProbe(new GuestSessionInfo(1, "WinSta0", HasInputDesktop: true)),
new GuestAgentIdentity("1.0.0", "hash", "arm64", 1, 1),
files: new GuestFileService(harness.GuestManaged),
files: ReportsManagedRoot ? new GuestFileService(harness.GuestManaged) : null,
guestWinapp: @"C:\WinAppGuest\winapp.exe",
appLauncher: appLauncher);

Expand Down
10 changes: 10 additions & 0 deletions src/winapp-CLI/WinApp.Cli/Commands/TargetSnapshotCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,9 @@ public override async Task<int> InvokeAsync(
Running = inspection.Running,
Attached = target is not null,
Capabilities = target?.Capabilities,
WorkRoot = string.IsNullOrWhiteSpace(target?.Capabilities.ManagedRoot)
? null
: GuestPaths.Resolve(target.Capabilities, TargetFileTransferService.WorkScope),
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
Desktop = DescribeDesktop(inspection.Running),
Deployments = [],
};
Expand Down Expand Up @@ -383,6 +386,10 @@ private static void Render(IAnsiConsole console, ExecutionTargetRef reference, T
console.MarkupLineInterpolated($" Architecture: {TerminalText.Sanitize(capabilities.Architecture)}");
console.MarkupLineInterpolated(
$" Guest support: real input {Yes(capabilities.SupportsRealInput)}, screen capture {Yes(capabilities.SupportsScreenCapture)}, interactive desktop {Yes(capabilities.SupportsInteractiveDesktop)}");
if (output.WorkRoot is { } workRoot)
{
console.MarkupLineInterpolated($" Work root: {TerminalText.Sanitize(workRoot)}");
}
}
else
{
Expand Down Expand Up @@ -491,6 +498,9 @@ internal sealed class TargetSnapshotOutput
/// <summary>What the live guest reports it can do, or null when no agent answered.</summary>
public ExecutionTargetCapabilities? Capabilities { get; init; }

/// <summary>Absolute base for relative transfer paths, or null when the guest root is unknown.</summary>
public string? WorkRoot { get; init; }

/// <summary>Where this target's desktop is drawn on this machine, if anywhere.</summary>
public required TargetSnapshotDesktop Desktop { get; init; }

Expand Down
Loading