Étend ServerExposureSettings avec autoStart (défaut false, mock aligné sur le backend). useDeployment expose setAutoStart, qui persiste immédiatement le draft sans jamais appeler start() ni toucher le comportement de stop(), et ne met à jour l'état local qu'après confirmation de la sauvegarde (même piste de validation que start/save). DeploymentSettings ajoute la case à cocher sous le panneau Serveur, et des actions « Modifier le port »/« Réessayer » quand le statut échoue avec le code PORT_IN_USE stabilisé côté backend. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
73 lines
2.5 KiB
TypeScript
73 lines
2.5 KiB
TypeScript
/**
|
|
* Ticket #68 — "Desktop only" is a code/test guard, not just a claim.
|
|
*
|
|
* The web client never mounts `ProjectsView` (it routes through `features/web`),
|
|
* so the Deployment surface is unreachable there *today*. This pins that: the
|
|
* web feature must not import the settings surface, and the web transport must
|
|
* refuse the embedded-server port rather than reach for Tauri (absent on web).
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
|
|
import { WebDesktopServerGateway } from "@/adapters/http/unsupported";
|
|
import type { GatewayError } from "@/domain";
|
|
|
|
const WEB_FEATURE_DIR = join(process.cwd(), "src", "features", "web");
|
|
|
|
function collectSourceFiles(dir: string, out: string[] = []): string[] {
|
|
for (const entry of readdirSync(dir)) {
|
|
const full = join(dir, entry);
|
|
if (statSync(full).isDirectory()) collectSourceFiles(full, out);
|
|
else if (/\.tsx?$/.test(entry)) out.push(full);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
describe("the Deployment surface is desktop-only", () => {
|
|
it("features/web does not import the settings surface", () => {
|
|
const offenders = collectSourceFiles(WEB_FEATURE_DIR).filter((file) =>
|
|
/@\/features\/settings/.test(readFileSync(file, "utf8")),
|
|
);
|
|
expect(offenders, `offending files: ${offenders.join(", ")}`).toEqual([]);
|
|
});
|
|
|
|
it("the web transport refuses the embedded-server port", async () => {
|
|
const gateway = new WebDesktopServerGateway();
|
|
|
|
// Every real operation fails explicitly, with a stable code the UI can branch on.
|
|
for (const call of [
|
|
() => gateway.getExposureSettings(),
|
|
() => gateway.status(),
|
|
() => gateway.start(),
|
|
() => gateway.stop(),
|
|
() =>
|
|
gateway.saveExposureSettings({
|
|
mode: "localOnly",
|
|
autoStart: false,
|
|
port: 0,
|
|
trustedProxies: [],
|
|
}),
|
|
() =>
|
|
gateway.previewExposure({
|
|
mode: "localOnly",
|
|
autoStart: false,
|
|
port: 0,
|
|
trustedProxies: [],
|
|
}),
|
|
]) {
|
|
const error: Partial<GatewayError> = { code: "UNSUPPORTED_ON_WEB" };
|
|
await expect(call()).rejects.toMatchObject(error);
|
|
}
|
|
});
|
|
|
|
it("web status subscription is inert rather than throwing", async () => {
|
|
// Teardown must stay callable: consumers unsubscribe unconditionally.
|
|
const unsubscribe = await new WebDesktopServerGateway().onStatusChanged(() => {
|
|
throw new Error("must never fire on web");
|
|
});
|
|
expect(() => unsubscribe()).not.toThrow();
|
|
});
|
|
});
|