Hold back the Disk panel and OS drag-out from the ship branch
This is a scope reduction, not an abandonment. Both subsystems are
preserved in full on `hold/disk-and-dragout` and are intended to come
back once they have been hardened separately. Nothing here is a
judgement that the features are unwanted — three successive
audit-and-fix cycles each closed a critical defect in these two areas
and each opened a new one, so the rest of the round ships now and these
two get their own cycle rather than holding it up.
Removed: the Disk settings panel and its whole reclaim / destroy /
compaction surface — `DiskSettings`, `DiskProjectTable`, `useDiskUsage`,
`docker/disk.rs`, `disk_tests.rs`, the disk commands in
`docker_commands.rs`, and their `generate_handler!` entries. Dropping
the IPC entries is the point: a UI-only removal would have left five
commands callable by a compromised webview, one of them a verified
arbitrary-DELETE primitive. `sweep_orphaned_snapshots`'s *command* goes
with them (the panel was its only caller); the sweep itself stays.
Removed: OS drag-out from the Files tab — `stage_container_file_for_drag`
and its host staging lifecycle, the pointer gesture and `dragPreview`,
`stageForDrag` / `isStagedHostPath`, the `tauri-plugin-drag` and
`@crabnebula/tauri-plugin-drag` dependencies, and the
`drag:allow-start-drag` capability grant, which could not be scoped.
The capability test's expected list is updated; its `*:default` and
`store:*` assertions are untouched.
Kept, deliberately: drag-and-drop *into* the app (Files pane and
terminal) and "Save to host…", which is now the only route out of a
container. The prevention work is untouched — the pre-commit scrub and
`SNAPSHOT_SCRUB_PATHS`, capped container logs, the `triple-c.base` /
`triple-c.managed` labels, `sweep_orphaned_snapshots` and the startup
housekeeping, the migration pin/probe reapers, scheduler log pruning,
`formatBytes.ts`, and `project_lock.rs` in full with every acquisition
site outside `disk.rs`.
Entanglements, resolved rather than deleted blind:
* `container.rs`'s `a_compaction_runs_this_module_s_scrub_script_byte_for_byte`
pinned the compaction Dockerfile against `snapshot_scrub_script()`.
Dropped — it existed only for compaction. `snapshot_scrub_script` and
its containment tests are untouched.
* `lib.rs`'s startup reap of `:compacting` tags and `triple-c-compact-*`
containers is dropped: nothing on this branch creates them.
* `project_lock`'s `Compaction` / `CacheClear` variants and
`any_held_excluding`, `migration_commands::is_migrating`, and
`formatBytes{Delta,Ceiling}` lose their last production caller but are
kept and still tested, annotated with why.
* `projects_store::corrupt_since` and `migration_store::peek_ownerless_since`
were read only by the disk survey and are removed. The corrupt-load
marker and `.bak` are still written.
Verified: `npm run test` 611 passing, `npx tsc --noEmit` clean,
`npm run build` green; `cargo test` 419 passed / 2 ignored,
`cargo build` 0 warnings. Every test removed belongs to a removed
feature — no kept-behaviour test was weakened or deleted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GBq2rGum6GX7xXgsas1fDc
This commit is contained in:
@@ -8,7 +8,6 @@ const downloadContainerFile = vi.fn();
|
||||
const uploadFileToContainer = vi.fn();
|
||||
const renameContainerPath = vi.fn();
|
||||
const createContainerDirectory = vi.fn();
|
||||
const stageContainerFileForDrag = vi.fn();
|
||||
|
||||
vi.mock("../lib/tauri-commands", () => ({
|
||||
listContainerFiles: (p: string, path: string) => listContainerFiles(p, path),
|
||||
@@ -18,7 +17,6 @@ vi.mock("../lib/tauri-commands", () => ({
|
||||
createContainerDirectory: (p: string, parent: string, n: string) =>
|
||||
createContainerDirectory(p, parent, n),
|
||||
readContainerFile: vi.fn(),
|
||||
stageContainerFileForDrag: (p: string, path: string) => stageContainerFileForDrag(p, path),
|
||||
}));
|
||||
|
||||
/**
|
||||
@@ -231,85 +229,6 @@ describe("useFileManager save to host", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("useFileManager drag-out staging", () => {
|
||||
it("copies the file onto the host and hands back the host path", async () => {
|
||||
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
let staged: { hostPath: string; cached: boolean } | null = null;
|
||||
await act(async () => {
|
||||
staged = await result.current.stageForDrag(file("a.txt"));
|
||||
});
|
||||
|
||||
expect(stageContainerFileForDrag).toHaveBeenCalledWith("p1", "/workspace/a.txt");
|
||||
expect(staged).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: false });
|
||||
// The note is transient — it must not still be sitting there afterwards.
|
||||
expect(result.current.busy).toBeNull();
|
||||
});
|
||||
|
||||
it("reuses the copy on a second drag of the same entry", async () => {
|
||||
// The whole point of the cache: the copy is the slow half of the gesture,
|
||||
// and a retry after a drag the OS missed has to be immediate.
|
||||
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
let second: { hostPath: string; cached: boolean } | null = null;
|
||||
await act(async () => {
|
||||
await result.current.stageForDrag(file("a.txt"));
|
||||
second = await result.current.stageForDrag(file("a.txt"));
|
||||
});
|
||||
|
||||
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(1);
|
||||
expect(second).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: true });
|
||||
});
|
||||
|
||||
it("re-stages once the entry has changed underneath it", async () => {
|
||||
// Keyed on size and mtime, so a file edited in the container is copied
|
||||
// again rather than dragged out at its old contents.
|
||||
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.stageForDrag(file("a.txt", { size: 10 }));
|
||||
await result.current.stageForDrag(file("a.txt", { size: 4096 }));
|
||||
});
|
||||
|
||||
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("surfaces a refused staging instead of returning a path that is not there", async () => {
|
||||
stageContainerFileForDrag.mockRejectedValue(
|
||||
'900 MB is too large to drag out (limit 256 MB) — use "Save to host…" instead.',
|
||||
);
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
let staged: { hostPath: string; cached: boolean } | null = null;
|
||||
await act(async () => {
|
||||
staged = await result.current.stageForDrag(file("huge.bin"));
|
||||
});
|
||||
|
||||
expect(staged).toBeNull();
|
||||
expect(toastText()).toContain("too large to drag out");
|
||||
expect(toastText()).toContain("Save to host");
|
||||
expect(result.current.busy).toBeNull();
|
||||
});
|
||||
|
||||
it("does not cache a failure, so a retry actually retries", async () => {
|
||||
stageContainerFileForDrag.mockRejectedValueOnce("Container not running");
|
||||
stageContainerFileForDrag.mockResolvedValueOnce("/tmp/triple-c-drag-out/s1/a.txt");
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
let staged: { hostPath: string; cached: boolean } | null = null;
|
||||
await act(async () => {
|
||||
await result.current.stageForDrag(file("a.txt"));
|
||||
staged = await result.current.stageForDrag(file("a.txt"));
|
||||
});
|
||||
|
||||
expect(stageContainerFileForDrag).toHaveBeenCalledTimes(2);
|
||||
expect(staged).toEqual({ hostPath: "/tmp/triple-c-drag-out/s1/a.txt", cached: false });
|
||||
});
|
||||
});
|
||||
|
||||
describe("useFileManager stays where the user is", () => {
|
||||
it("does not drag the pane back when the user navigates away mid-upload", async () => {
|
||||
// The closure captured `/workspace`; the user is in `/workspace/src` by the
|
||||
@@ -485,21 +404,6 @@ describe("useFileManager overwrite prompt", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("useFileManager staged host paths", () => {
|
||||
it("recognises a path it staged, and only that path", async () => {
|
||||
stageContainerFileForDrag.mockResolvedValue("/tmp/triple-c-drag-out/s1/a.txt");
|
||||
const { result } = renderHook(() => useFileManager("p1"));
|
||||
|
||||
expect(result.current.isStagedHostPath("/tmp/triple-c-drag-out/s1/a.txt")).toBe(false);
|
||||
await act(async () => {
|
||||
await result.current.stageForDrag(file("a.txt"));
|
||||
});
|
||||
expect(result.current.isStagedHostPath("/tmp/triple-c-drag-out/s1/a.txt")).toBe(true);
|
||||
// Same basename, a real host file the user actually wants uploaded.
|
||||
expect(result.current.isStagedHostPath("/home/me/a.txt")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The loop, end to end. The prompt only earns its place if the *batch* survives
|
||||
* it: one answer, given once, has to leave every other file in the drop exactly
|
||||
|
||||
Reference in New Issue
Block a user