Secret Scan / scan (push) Successful in 7s
Build App (Preview) / compute-version (pull_request) Successful in 12s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / test (pull_request) Successful in 4m45s
Build App (Preview) / create-release (pull_request) Successful in 3s
Build App (Preview) / build-macos (pull_request) Successful in 3m16s
Build App (Preview) / build-linux (pull_request) Successful in 5m36s
Build App (Preview) / build-windows (pull_request) Failing after 6m38s
Build App (Preview) / prune-previews (pull_request) Skipped
Build Container / build-container (pull_request) Successful in 23m46s
The entrypoint ended in `exec su -s /bin/bash claude -c "exec sleep infinity"`, so a project container's whole lifetime was a sleep owned by the unprivileged claude user. Claude Code runs as claude and routinely runs `pkill sleep`, `pkill -f`, `kill -9 -1` while testing, and any of those stopped the container: every terminal disconnected and the app kept showing Running. Entrypoint: stay resident as root and wait on a root-owned sleep run as `triple-c-keepalive infinity`, respawned if killed, with a TERM/INT/HUP trap so `docker stop` returns at once (550 ms, exit 0, vs 2.6 s / 143 before). Unprivileged kills now get EPERM; `sudo pkill sleep` is survived by the respawn. The drop to claude dated from the initial commit with no stated purpose; its only reader was the marketplace readiness probe, which now accepts either generation of entrypoint. App: container_watch.rs holds one Docker event subscription filtered daemon-side to container `die`. A die naming a Running project's container is confirmed with an inspect, then ProjectsStore::mark_exited flips it to Stopped as a compare-and-set that skips any project a command holds the project lock on. Each (re)subscription re-checks Running projects and asks for events since just before it. The frontend applies the new `project-status-changed` event and toasts why the sessions died. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
49 lines
1.8 KiB
TypeScript
49 lines
1.8 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { renderHook } from "@testing-library/react";
|
|
import { useAppState } from "../store/appState";
|
|
import type { Project } from "../lib/types";
|
|
|
|
let handler: ((e: { payload: unknown }) => void) | null = null;
|
|
const unlisten = vi.fn();
|
|
vi.mock("@tauri-apps/api/event", () => ({
|
|
listen: vi.fn(async (name: string, cb: (e: { payload: unknown }) => void) => {
|
|
if (name === "project-status-changed") handler = cb;
|
|
return unlisten;
|
|
}),
|
|
}));
|
|
|
|
import { useProjectStatusEvents } from "./useProjectStatusEvents";
|
|
|
|
const project = (over: Partial<Project>): Project =>
|
|
({ id: "p1", name: "demo", status: "running", container_id: "c1", ...over }) as Project;
|
|
|
|
describe("useProjectStatusEvents", () => {
|
|
beforeEach(() => {
|
|
handler = null;
|
|
unlisten.mockClear();
|
|
useAppState.setState({ projects: [project({}), project({ id: "p2", name: "other" })], toasts: [] });
|
|
});
|
|
|
|
it("replaces the project the backend reports, so a dead container stops reading Running", async () => {
|
|
renderHook(() => useProjectStatusEvents());
|
|
await vi.waitFor(() => expect(handler).not.toBeNull());
|
|
|
|
handler!({ payload: project({ status: "stopped" }) });
|
|
|
|
const { projects, toasts } = useAppState.getState();
|
|
expect(projects.find((p) => p.id === "p1")?.status).toBe("stopped");
|
|
expect(projects.find((p) => p.id === "p2")?.status).toBe("running");
|
|
// The terminals just went dead with no explanation; say why.
|
|
expect(toasts).toHaveLength(1);
|
|
expect(toasts[0].message).toContain("demo");
|
|
expect(toasts[0].dedupeKey).toBe("container-exited-p1");
|
|
});
|
|
|
|
it("stops listening on unmount", async () => {
|
|
const { unmount } = renderHook(() => useProjectStatusEvents());
|
|
await vi.waitFor(() => expect(handler).not.toBeNull());
|
|
unmount();
|
|
expect(unlisten).toHaveBeenCalled();
|
|
});
|
|
});
|