fix: keep running containers until stack pull/build succeeds (#1657)

* fix: keep running containers until stack pull/build succeeds

Acquire images before reconcile, capture a recovery generation for
compensation, and only remove classified orphans after handoff.

* fix: address recovery audit blockers for safe stack updates

Retire abandoned and expired recovery artifacts, probe compensated
runtimes before reporting rollback success, preserve local Docker when
deleting a node, validate the exact Compose invocation before capture,
and repair updateStack return-contract fixtures.

* fix: resolve ESLint errors blocking CI on this branch

Unused-import and unused-variable errors left over from the stack
deletion refactor: MeshService in stacks.ts (its opt-out cascade moved
into DeployedStackDeletionService), a redundant pruneVolumes
destructure in deleteDeployedStack (the real one is re-derived from
the same input object inside runDeletionBody), and an unused beforeAll
import in a Docker-integration test stub. Also scopes the webhook
pull-action case body in a block to satisfy no-case-declarations;
purely syntactic, no behavior change.

* fix: harden recovery probe, cleanup retry, and failed-pull Docker test

Reject absent or unhealthy expected replicas before reporting rollback
success, keep cleanup records until artifacts are actually removed, fail
closed when a mesh override cannot be generated, and assert a real
failed pull leaves the original container running.

* fix: verify recovery probe image identity and stack-scoped override paths

Reject recovered runtimes that use the wrong image or leave scale-zero
services running, and confine tombstone override deletion to the intent
stack directory so forged cross-stack paths cannot be swept.

* test: batch notification cap fixtures in a SQLite transaction

Unbatched 1200-row inserts were timing out at the default 30s under
CI load even though the same assertions pass in under 2s when green.
This commit is contained in:
Anso
2026-07-21 12:18:01 -04:00
committed by GitHub
parent b1decbb32a
commit 3f1f15a6f4
41 changed files with 3087 additions and 244 deletions
@@ -19,6 +19,7 @@ vi.mock('@/components/ui/toast-store', () => ({
}));
import { apiFetch } from '@/lib/api';
import { toast } from '@/components/ui/toast-store';
type EditorState = ReturnType<typeof useEditorViewState>;
type StackListState = ReturnType<typeof useStackListState>;
@@ -314,6 +315,18 @@ describe('useStackActions policy-block dialog wiring', () => {
expect(overlayState.setPolicyBlock).not.toHaveBeenCalled();
});
it('toasts a deletion-in-progress message when the conflict action is delete', async () => {
const inProgress = JSON.stringify({
code: 'stack_op_in_progress',
inProgress: { action: 'delete', startedAt: Date.now(), user: 'admin' },
});
vi.mocked(apiFetch).mockResolvedValueOnce(new Response(inProgress, { status: 409 }));
const { result, overlayState } = setup();
await act(async () => { await result.current.updateStack(mouseEvent); });
expect(overlayState.setPolicyBlock).not.toHaveBeenCalled();
expect(toast.error).toHaveBeenCalledWith(expect.stringMatching(/already deleting/i));
});
it('opens the dialog with action "update" via the sidebar update entry point', async () => {
vi.mocked(apiFetch).mockResolvedValueOnce(new Response(JSON.stringify(policyPayload), { status: 409 }));
const { result, overlayState } = setup();
@@ -107,7 +107,7 @@ const parseFailureClassification = (value: unknown): FailureClassification | und
return undefined;
};
type StackOpAction = 'deploy' | 'down' | 'restart' | 'stop' | 'start' | 'update';
type StackOpAction = 'deploy' | 'down' | 'restart' | 'stop' | 'start' | 'update' | 'delete';
interface StackOpInProgressInfo {
action: StackOpAction;
@@ -122,6 +122,7 @@ const STACK_OP_PRESENT_PARTICIPLE: Record<StackOpAction, string> = {
stop: 'stopping',
start: 'starting',
update: 'updating',
delete: 'deleting',
};
const VALID_STACK_OP_ACTIONS: ReadonlySet<string> = new Set(
@@ -38,7 +38,7 @@ export function classifyOperationPhase(rows: ParsedLogRow[], action: ActionVerb)
if (stage === 'BUILD') {
return 'Building images';
}
if (message.includes('Backup created for atomic') || message.includes('Cleaning up existing containers')) {
if (message.includes('Backup created for atomic') || message.includes('Validating stack for update') || message.includes('Capturing rollback generation')) {
return 'Preparing';
}
}