Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions apps/mobile/src/jones/updates/JonesUpdateControls.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,59 @@ afterEach(() => {
});

describe("mobile host update controls", () => {
it("shows a dated failed check without classification and clears generic failure after success", async () => {
const failed = {
...state(),
phase: "no-new" as const,
lastCheck: {
outcome: "failed" as const,
at: "2026-07-10T15:05:00Z",
reason: "Private synthetic host reason",
},
};
await render(failed);
expect(container.textContent).toContain(
"The update check failed. Check the connection or authentication. Checked Jul 10, 2026, 11:05 AM EDT.",
);
expect(container.textContent).not.toContain(failed.lastCheck.reason);
const classified = {
...failed,
checkClassification: { kind: "failure" as const, checkedAt: stamp },
};
await render(classified);
expect(container.textContent).toContain("The update check failed.");
await render({
...classified,
lastCheck: { outcome: "succeeded", at: "2026-07-10T15:10:00Z" },
});
expect(container.textContent).not.toContain("The update check failed.");
});
it("disables a tree-only already-running stage without Alert or dispatch and keeps protected messages", async () => {
const staged = { ...state(true), stagedAlreadyRunning: true as const };
const message = "The downloaded build is already running on this host. Nothing to install.";
await render(staged);
expect(button("Install downloaded build").disabled).toBe(true);
await click("Install downloaded build");
expect(harness.alert).not.toHaveBeenCalled();
expect(harness.operation).not.toHaveBeenCalled();
expect(harness.action).not.toHaveBeenCalled();
expect(container.textContent).toContain(message);
await render({
...staged,
operation: {
...running(),
phase: "held",
code: "launcher-unconfirmed",
message: "Read this exact protected outcome.",
},
});
expect(container.textContent).toContain("Stop not confirmed · launcher-unconfirmed");
expect(container.textContent).toContain("Read this exact protected outcome.");
expect(container.textContent).toContain(message);
await render(staged, "stale");
expect(container.textContent).not.toContain(message);
});

const alternativeState = () => {
const retained = state(true);
return {
Expand Down
19 changes: 16 additions & 3 deletions apps/mobile/src/jones/updates/JonesUpdateControls.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { jonesBuildLabels, type JonesBuildRef } from "@t3tools/shared/jones/buil
import { JonesBuildVersion } from "./JonesBuildVersion";
import {
jonesCandidatePresentation,
jonesStagedAlreadyRunning,
JONES_STAGED_ALREADY_RUNNING_MESSAGE,
jonesAlternativePresentation,
resolveJonesAlternativeAction,
jonesFollowerHold,
Expand All @@ -25,7 +27,10 @@ import {
JonesRecoveryPreflight,
} from "@t3tools/contracts/jones/jonesOperations";
import type { AtomCommandResult } from "@t3tools/client-runtime/state/runtime";
import { jonesTargetStatus } from "@t3tools/client-runtime/jones/updates/target-status";
import {
jonesCheckOutcome,
jonesTargetStatus,
} from "@t3tools/client-runtime/jones/updates/target-status";
import { View } from "react-native";
import { AppText as Text } from "../../components/ui/AppText";
import {
Expand Down Expand Up @@ -122,6 +127,7 @@ export function JonesUpdateControls({
);
const observation = mobileJonesObservation(observed, saved);
const state = observation.state;
const check = jonesCheckOutcome(state);
const presentation = state === null ? undefined : jonesUpdatePresentation(state);
const status = jonesTargetStatus(observation, {
canOperate: allowed && state?.environmentId === environmentId,
Expand Down Expand Up @@ -190,8 +196,8 @@ export function JonesUpdateControls({
{saved.fault ?? "Reading update request history…"}
</Text>
) : null}
{status.check !== undefined ? (
<Text className="text-sm text-foreground-muted">{status.check.text}</Text>
{check !== undefined ? (
<Text className="text-sm text-foreground-muted">{check.text}</Text>
) : null}
{displayedOperation !== undefined ? (
<>
Expand Down Expand Up @@ -220,6 +226,13 @@ export function JonesUpdateControls({
will be resent.
</Text>
) : null}
{observation.freshness === "fresh" &&
state?.environmentId === environmentId &&
jonesStagedAlreadyRunning(state) ? (
<Text className="text-sm text-foreground-muted">
{JONES_STAGED_ALREADY_RUNNING_MESSAGE}
</Text>
) : null}
{followerSummary !== undefined ? (
<Text className="text-sm text-foreground-muted">
{followerHold === undefined ? followerSummary : followerHold}
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/jones/updates/JonesBuildStatus.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,33 @@ const precedes = (first: Node, second: Node) =>
(first.compareDocumentPosition(second) & Node.DOCUMENT_POSITION_FOLLOWING) !== 0;

describe("Jones build status", () => {
it("disables an already-running stage without dispatch and explains fresh bound proof", async () => {
const state = {
...available,
phase: "staged" as const,
stagedHandle: "stage",
stagedAlreadyRunning: true as const,
};
const message = "The downloaded build is already running on this host. Nothing to install.";
await using screen = await renderStatus({ state });
const install = buttonNamed(screen.container, "Install");
expect(install.disabled).toBe(true);
await act(async () => install.click());
expect(screen.onRequest).not.toHaveBeenCalled();
expect(screen.container.querySelector('[role="status"]')?.textContent).toBe(message);
await screen.rerender(
statusUi({ state, check: { kind: "failure", text: "The update check failed." } }),
);
expect(screen.container.textContent).toContain("The update check failed.");
expect(screen.container.textContent).not.toContain(message);
await act(async () => buttonNamed(screen.container, "Details").click());
expect(screen.container.textContent).toContain(message);
await screen.rerender(statusUi({ state, freshness: "stale" }));
expect(screen.container.textContent).not.toContain(message);
await screen.rerender(statusUi({ state, environmentId: EnvironmentId.make("other-host") }));
expect(screen.container.textContent).not.toContain(message);
});

it("shows retained and alternative builds at narrow width and replaces only after exact confirmation", async () => {
const retained = {
...available,
Expand Down
14 changes: 13 additions & 1 deletion apps/web/src/jones/updates/JonesBuildStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {
jonesCandidatePresentation,
jonesStagedAlreadyRunning,
JONES_STAGED_ALREADY_RUNNING_MESSAGE,
jonesAlternativePresentation,
jonesFollowerHold,
jonesFollowerSummary,
Expand Down Expand Up @@ -221,6 +223,7 @@ export function JonesBuildStatus({
);
}

const alreadyRunning = jonesStagedAlreadyRunning(state);
const presentation = jonesUpdatePresentation(state);
const candidate = jonesCandidatePresentation(state);
const alternative = jonesAlternativePresentation(state);
Expand Down Expand Up @@ -392,6 +395,9 @@ export function JonesBuildStatus({
// An operation's own phase (stopped, restarting); its idle headline repeats the host message.
operation !== undefined &&
operation.headline !== presentation.message && { text: operation.headline, tone: "muted" },
freshness === "fresh" &&
state.environmentId === environmentId &&
alreadyRunning && { text: JONES_STAGED_ALREADY_RUNNING_MESSAGE, tone: "muted" },
followerHold !== undefined &&
candidate !== undefined && {
text:
Expand Down Expand Up @@ -492,12 +498,18 @@ export function JonesBuildStatus({
size={styles.button}
disabled={
busy ||
alreadyRunning ||
!state.capability.install ||
state.currentVersion === undefined ||
followerHold !== undefined
}
onClick={() => {
if (state.stagedHandle === undefined || state.currentVersion === undefined) return;
if (
alreadyRunning ||
state.stagedHandle === undefined ||
state.currentVersion === undefined
)
return;
onRequest({
action: "install",
input: {
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/jones/updates/JonesUpdateControls.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { jonesTargetStatus } from "@t3tools/client-runtime/jones/updates/target-status";
import {
jonesCheckOutcome,
jonesTargetStatus,
} from "@t3tools/client-runtime/jones/updates/target-status";
import {
prepareReviewedInstall,
resolveDownloadAction,
Expand Down Expand Up @@ -312,7 +315,7 @@ export function JonesUpdateControls({
}
actionError={actionError}
layout={layout}
check={status.check}
check={jonesCheckOutcome(observation.state)}
operation={
status.operation === undefined
? undefined
Expand Down
51 changes: 51 additions & 0 deletions apps/web/src/jones/updates/fixtures/environment-updates-entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { StrictMode, useState } from "react";
import { createRoot } from "react-dom/client";
import { createRootRoute, createRouter, RouterProvider } from "@tanstack/react-router";
import { EnvironmentId, type EnvironmentMachineKind } from "@t3tools/contracts";
import { jonesCheckOutcome } from "@t3tools/client-runtime/jones/updates/target-status";
import type { JonesUpdateState } from "@t3tools/contracts/jones/jonesUpdates";
import { AUTH_SCOPE_OPTIONS } from "@t3tools/shared/authScopeOptions";
import "../../../index.css";
Expand Down Expand Up @@ -88,6 +89,55 @@ interface Scenario {
function scenarios(): readonly Scenario[] {
const id = (key: string) => EnvironmentId.make(`fixture-${key}`);
return [
{
key: "already-running",
label: "Already running host",
kind: "server",
subtitle: "Synthetic host · Connected",
routes: [{ label: "SSH", address: "fixture-already-running", inUse: true }],
build: {
state: state(id("already-running"), {
phase: "staged",
provenance: offered,
stagedHandle: "staged-already-running",
stagedAlreadyRunning: true,
}),
freshness: "fresh",
},
fleet: { enrolled: false, readiness: "qualified" },
},
{
key: "dated-check-failure",
label: "Failed check host",
kind: "server",
subtitle: "Synthetic host · Connected",
routes: [{ label: "SSH", address: "fixture-check-failure", inUse: true }],
build: {
state: state(id("dated-check-failure"), {
phase: "no-new",
checkClassification: { kind: "failure", checkedAt: "2026-11-10T15:00:00Z" },
lastCheck: { outcome: "failed", at: "2026-11-10T15:05:00Z" },
}),
freshness: "fresh",
},
fleet: { enrolled: false, readiness: "qualified" },
},
{
key: "successful-recheck",
label: "Successful recheck host",
kind: "server",
subtitle: "Synthetic host · Connected",
routes: [{ label: "SSH", address: "fixture-successful-recheck", inUse: true }],
build: {
state: state(id("successful-recheck"), {
phase: "no-new",
checkClassification: { kind: "failure", checkedAt: "2026-11-10T15:00:00Z" },
lastCheck: { outcome: "succeeded", at: "2026-11-10T15:10:00Z" },
}),
freshness: "fresh",
},
fleet: { enrolled: false, readiness: "qualified" },
},
{
key: "reconnecting",
label: "MJones’s Mac mini",
Expand Down Expand Up @@ -484,6 +534,7 @@ function ScenarioRow({
environmentId={environmentId}
state={scenario.build.state}
freshness={scenario.build.freshness}
check={jonesCheckOutcome(scenario.build.state)}
freshnessMessage={scenario.build.freshnessMessage}
comparison={scenario.build.comparison ?? null}
installedComparison={scenario.build.installedComparison ?? null}
Expand Down
57 changes: 55 additions & 2 deletions apps/web/src/jones/updates/productionReview.browser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,10 +306,56 @@ describe("production web review authority", () => {
expect(h.legacy).not.toHaveBeenCalled();
expect(h.native).not.toHaveBeenCalled();
});
it("dates a Connections failure by its check, then clears stale failure after a successful recheck", async () => {
await using _fixture = await fixture();
const { a } = hosts();
const failed = {
...h.observations.get(a)!.state!,
phase: "no-new" as const,
checkClassification: { kind: "failure" as const, checkedAt: "2026-11-10T15:00:00Z" },
lastCheck: { outcome: "failed" as const, at: "2026-11-10T15:05:00Z" },
};
h.observations.set(a, { state: failed, freshness: "fresh" });
await draw("connections", a);
expect(container.textContent).toContain(
"The update check failed. Check the connection or authentication. Checked Nov 10, 2026, 10:05 AM EST.",
);
h.observations.set(a, {
state: { ...failed, lastCheck: { outcome: "succeeded", at: "2026-11-10T15:10:00Z" } },
freshness: "fresh",
});
await draw("connections", a);
await click("Details");
expect(container.textContent).not.toContain("The update check failed.");
const { lastCheck: _lastCheck, ...oldHost } = failed;
h.observations.set(a, { state: oldHost, freshness: "fresh" });
await draw("connections", a);
expect(container.textContent).toContain(
"The update check failed. Check the connection or authentication. Checked Nov 10, 2026, 10:00 AM EST.",
);
});
it("does not open Connections confirmation for an already-running stage", async () => {
await using _fixture = await fixture();
const { a } = hosts();
const observed = h.observations.get(a)!;
h.observations.set(a, {
...observed,
state: { ...observed.state!, stagedAlreadyRunning: true },
});
await draw("connections", a);
const install = [...container.querySelectorAll("button")].find(
(node) => node.textContent === "Install",
)!;
expect(install.disabled).toBe(true);
await click("Install");
expect(h.confirmations).toHaveLength(0);
expect(h.operation).not.toHaveBeenCalled();
expect(h.legacy).not.toHaveBeenCalled();
});
it.each(
(["connections", "exact"] as const).flatMap((control) =>
[true, false].flatMap((modern) =>
(["retarget", "unmount", "permission"] as const).map((transition) => ({
(["retarget", "unmount", "permission", "already-running"] as const).map((transition) => ({
control,
modern,
transition,
Expand All @@ -327,7 +373,14 @@ describe("production web review authority", () => {
expect(h.confirmations).toHaveLength(1);
if (transition === "retarget") await draw(control, b);
else if (transition === "unmount") await root.rerender(null);
else {
else if (transition === "already-running") {
const observed = h.observations.get(a)!;
h.observations.set(a, {
...observed,
state: { ...observed.state!, stagedAlreadyRunning: true },
});
await draw(control, a);
} else {
h.permissions.set(a, false);
await draw(control, a);
}
Expand Down
Loading
Loading