fix: show pending and unknown work folder save state
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
30ed3cbc99
commit
ebe1fc8b65
|
|
@ -79,6 +79,9 @@ are unchanged.
|
|||
CLI state is separate from the four shared collections. A change of task,
|
||||
agent, responsible user, or project cannot reuse a sandbox with another binding.
|
||||
|
||||
The cache inspector shows “Waiting for first save” until an active run has a
|
||||
completed checkpoint. Missing or unavailable sync status is never labeled saved.
|
||||
|
||||
## Storage and synchronization
|
||||
|
||||
Postgres stores company/owner bindings, paths, executable bits, current object
|
||||
|
|
|
|||
|
|
@ -10,16 +10,32 @@ vi.mock("@/context/CompanyContext", () => ({ useCompany: () => ({ selectedCompan
|
|||
|
||||
const owner = { companyId: "company", scope: "task" as const, ownerId: "task" };
|
||||
const key = ["work-folders", owner.companyId, owner.scope, owner.ownerId];
|
||||
function render(statuses: WorkFolderSyncStatus[], lastOperationAt: string | null, readOnly = false) {
|
||||
function render(statuses: WorkFolderSyncStatus[], lastOperationAt: string | null, readOnly = false, queryState?: "loading" | "error") {
|
||||
const client = new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } });
|
||||
client.setQueryData([...key, "files", false], { files: [], lastOperationAt });
|
||||
client.setQueryData([...key, "sync"], statuses);
|
||||
if (queryState === "loading") client.removeQueries({ queryKey: [...key, "sync"] });
|
||||
if (queryState === "error") client.getQueryCache().find({ queryKey: [...key, "sync"] })!
|
||||
.setState({ status: "error", error: new Error("Save service unavailable") });
|
||||
return renderToStaticMarkup(<MemoryRouter initialEntries={["/STG/issues/task"]}><QueryClientProvider client={client}><TooltipProvider><WorkFolderBrowser owner={owner} readOnly={readOnly} /></TooltipProvider></QueryClientProvider></MemoryRouter>);
|
||||
}
|
||||
const checkpoint: WorkFolderSyncStatus = { runId: "run", state: "saved", active: false,
|
||||
lastSavedAt: "2026-09-07T12:00:00.000Z", error: null, refreshRequested: false };
|
||||
|
||||
describe("work folder save feedback", () => {
|
||||
it("does not claim a save before the first checkpoint", () => {
|
||||
const pending = render([{ ...checkpoint, state: "starting", active: true, lastSavedAt: null }], null, true);
|
||||
expect(pending).toContain('role="status">Waiting for first save');
|
||||
expect(pending).not.toContain("Last agent save");
|
||||
expect(render([], null, true)).toContain('role="status">No saved files');
|
||||
});
|
||||
it("does not report success while save status is unknown", () => {
|
||||
expect(render([], null, true, "loading")).toContain('role="status">Loading save status…');
|
||||
const unavailable = render([checkpoint], null, true, "error");
|
||||
expect(unavailable).toContain('role="status">Save status unavailable');
|
||||
expect(unavailable).toContain("Last agent save");
|
||||
expect(unavailable).toContain("Save service unavailable");
|
||||
});
|
||||
it("keeps inspection free of controls that mutate the cache or refresh the sandbox", () => {
|
||||
const html = render([checkpoint], null, true);
|
||||
for (const action of ["Upload", "Create folder", "Refresh sandbox", "Delete", "Restore", "Purge"]) expect(html).not.toContain(action);
|
||||
|
|
|
|||
|
|
@ -75,6 +75,15 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH
|
|||
const lastSaved = statuses.map((status) => status.lastSavedAt)
|
||||
.filter((value): value is string => Boolean(value)).sort().at(-1);
|
||||
const lastOperation = filesQuery.data?.lastOperationAt;
|
||||
const saveLabel = mutation.isPending ? "Saving…"
|
||||
: mutation.isError ? "Save failed"
|
||||
: syncQuery.isError || filesQuery.isError ? "Save status unavailable"
|
||||
: !exampleFiles && (syncQuery.isPending || filesQuery.isPending) ? "Loading save status…"
|
||||
: saving ? "Saving…"
|
||||
: failures.length > 0 ? "Run save failed"
|
||||
: !lastSaved && statuses.some((status) => status.active) ? "Waiting for first save"
|
||||
: lastSaved || lastOperation || files.length > 0 ? "Saved"
|
||||
: "No saved files";
|
||||
const disabled = mutation.isPending || Boolean(exampleFiles);
|
||||
return <Tabs value={trash ? "trash" : "files"} onValueChange={(value) => { setTrash(value === "trash"); setSelectedPath(null); setCheckedFiles(new Set()); }} className={cn("flex min-h-0 flex-col gap-3", fillHeight && "flex-1")}>
|
||||
<div className="flex shrink-0 flex-wrap items-center gap-2">
|
||||
|
|
@ -89,7 +98,7 @@ export function WorkFolderBrowser({ owner, exampleFiles, readOnly = false, fillH
|
|||
</TabsList>
|
||||
{!trash && canManageTrash && checkedPaths.length > 0 && <Button variant="outline" size="sm" disabled={disabled} onClick={() => mutation.mutate({ type: "deleteSelected", paths: checkedPaths })}><Trash2 aria-hidden />Move {checkedPaths.length} {checkedPaths.length === 1 ? "file" : "files"} to trash</Button>}
|
||||
{!readOnly && <Button variant="outline" size="sm" disabled={disabled || !statuses.some((status) => status.active)} onClick={() => mutation.mutate({ type: "refresh" })}><RefreshCw aria-hidden />Refresh sandbox</Button>}
|
||||
<span className="text-xs text-muted-foreground" role="status">{saving ? "Saving…" : mutation.isError ? "Save failed" : failures.length > 0 ? "Run save failed" : "Saved"}</span>
|
||||
<span className="text-xs text-muted-foreground" role="status">{saveLabel}</span>
|
||||
{lastSaved && <span className="text-xs text-muted-foreground">Last agent save {new Date(lastSaved).toLocaleTimeString()}</span>}
|
||||
{lastOperation && (!lastSaved || lastOperation > lastSaved) && <span className="text-xs text-muted-foreground">Files updated {new Date(lastOperation).toLocaleTimeString()}</span>}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type {
|
|||
export type WorkFolderScenario =
|
||||
| "saved"
|
||||
| "saving"
|
||||
| "waiting"
|
||||
| "failed"
|
||||
| "olderFailure"
|
||||
| "multipleFailures"
|
||||
|
|
@ -84,7 +85,7 @@ export function createWorkFolderFixture(
|
|||
});
|
||||
contents.set(path, blob);
|
||||
}
|
||||
if (scenario !== "empty") {
|
||||
if (scenario !== "empty" && scenario !== "waiting") {
|
||||
add(
|
||||
"README.md",
|
||||
`# ${workFolderLabels[scope as WorkFolderScope]}\n\nFiles available to this ${scope}'s sandbox runs.\n\n## Current work\n\n- Review the launch brief\n- Keep the original uploads\n- Share the final report\n`,
|
||||
|
|
@ -151,13 +152,13 @@ export function createWorkFolderFixture(
|
|||
runId: "run-storybook",
|
||||
agentId: workFolderOwners.agent.ownerId,
|
||||
state:
|
||||
scenario === "saving"
|
||||
scenario === "waiting" ? "starting" : scenario === "saving"
|
||||
? "saving"
|
||||
: scenario === "failed"
|
||||
? "failed"
|
||||
: "saved",
|
||||
lastSavedAt: savedAt,
|
||||
active: scenario === "saving" || scenario === "failed",
|
||||
lastSavedAt: scenario === "waiting" ? null : savedAt,
|
||||
active: scenario === "waiting" || scenario === "saving" || scenario === "failed",
|
||||
refreshRequested: false,
|
||||
error:
|
||||
scenario === "failed"
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ const meta = {
|
|||
options: [
|
||||
"saved",
|
||||
"saving",
|
||||
"waiting",
|
||||
"failed",
|
||||
"olderFailure",
|
||||
"multipleFailures",
|
||||
|
|
@ -100,6 +101,7 @@ export const LargeFilePreview: Story = {
|
|||
};
|
||||
export const EmptyFolder: Story = { args: { scenario: "empty" } };
|
||||
export const Loading: Story = { args: { scenario: "loading" } };
|
||||
export const WaitingForFirstSave: Story = { args: { scenario: "waiting" } };
|
||||
export const Saving: Story = { args: { scenario: "saving" } };
|
||||
export const SaveFailed: Story = { args: { scenario: "failed" } };
|
||||
export const SharedFolderWithEarlierFailure: Story = { args: { scope: "agent", scenario: "olderFailure" } };
|
||||
|
|
|
|||
Loading…
Reference in New Issue