c5d678a37a
Every spec drives the real sshd container set up by the ssh-test-base fixture; no mocks. Specs are grouped by concern (file per group), mirroring the docker/ directory style. Each one maps 1:1 to a row in docs/specs/ssh-executor/e2e-plan.md. UI surface: - connection-form.spec.ts — field rendering, save/test gating, identity-source toggle, default port. - test-result.spec.ts — successful + failed Test Connection paths, per-step badges, fingerprint surfacing, cached/will-upload copy. - trust-gate.spec.ts — save disabled until trust ticked, edits reset result + trust, fingerprint-change warning when pinned != observed. - sessions-card.spec.ts — empty state, row rendering, manual refresh, status badges, truncation, polling pickup. - persistence.spec.ts — fingerprint persists across reload, new form starts blank, executor list label, partial state doesn't leak. API contract: - test-endpoint.spec.ts — POST /api/v1/ssh/test step ordering, invalid body, host-required, handshake short-circuit, cached/uploaded states. - sessions-endpoint.spec.ts — GET /api/v1/ssh/executors/:id/sessions filters to ssh-runtime rows, populates metadata, monotonic uptime. - executor-crud.spec.ts — type=ssh persists fingerprint, profile workdir round-trip, edit-with-running-sessions keeps snapshots. Real-SSH integration: - launch-task.spec.ts — full smoke: agentctl upload + sha256 cache hit on second launch, task/session dir layout, stop cleans session dir. - recovery.spec.ts — backend restart reconnects, dead pid surfaces failure, persisted metadata keys present. - auth-methods.spec.ts — key + valid, missing key, passphrase-protected rejection, agent without SSH_AUTH_SOCK, wrong-key auth failure. - concurrency.spec.ts — two sessions share task dir distinct ports, two tasks distinct dirs, traffic drop + restore exercises eviction + reconnect. - hostkey-rotation.spec.ts — regenerate host key surfaces new fingerprint, stale pinned fingerprint blocks launch verbatim, re-trust restores function. - config-inheritance.spec.ts — ~/.ssh/config HostName / Port / IdentityFile / ProxyJump inheritance with form-overrides-config rules. - workdir-per-profile.spec.ts — per-profile ssh_workdir_root routes to separate remote workdir trees; default falls back to ~/.kandev. - proxy-jump.spec.ts — 2-container bastion topology asserts target is unreachable directly, reachable via ProxyJump, chained ProxyJump fails. - error-surfacing.spec.ts — TCP refused, auth failed, bastion unreachable surface verbatim; backend 400 stays a 400 not a synthesised failure. Also adds an AGENTS.md (CLAUDE.md symlink) line pointing at the e2e/README.md so future agents discover the containers project + KANDEV_E2E_CONTAINERS env var without grepping playwright.config.ts.
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
import { test, expect } from "../../fixtures/ssh-test-base";
|
||
import { SSHSettingsPage } from "../../pages/SSHSettingsPage";
|
||
|
||
/**
|
||
* UI form validation for the SSH connection card. None of these tests need
|
||
* to *connect* to the host; they assert pure form behaviour. Live on the
|
||
* `containers` project anyway so the SSH UI lives in one place.
|
||
*
|
||
* Covers e2e-plan.md group A (A1–A7).
|
||
*/
|
||
test.describe("ssh connection form validation", () => {
|
||
test("renders all expected fields", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
|
||
await expect(page.nameInput).toBeVisible();
|
||
await expect(page.hostAliasInput).toBeVisible();
|
||
await expect(page.hostInput).toBeVisible();
|
||
await expect(page.portInput).toBeVisible();
|
||
await expect(page.userInput).toBeVisible();
|
||
await expect(page.identitySourceTrigger).toBeVisible();
|
||
await expect(page.proxyJumpInput).toBeVisible();
|
||
// Identity file field is conditional: agent default hides it.
|
||
await expect(page.identityFileInput).toBeHidden();
|
||
});
|
||
|
||
test("save disabled without a name", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
await expect(page.saveButton).toBeDisabled();
|
||
|
||
await page.fillForm({ host: "dev.example.com" });
|
||
await expect(page.saveButton).toBeDisabled(); // no name still
|
||
await expect(page.testButton).toBeDisabled();
|
||
});
|
||
|
||
test("save and test disabled without host or alias", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
await page.fillForm({ name: "blank-host" });
|
||
await expect(page.testButton).toBeDisabled();
|
||
await expect(page.saveButton).toBeDisabled();
|
||
});
|
||
|
||
test("test enabled once name + host present", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
await page.fillForm({ name: "ready", host: "dev.example.com" });
|
||
await expect(page.testButton).toBeEnabled();
|
||
// Save still disabled until a test result + trust tick.
|
||
await expect(page.saveButton).toBeDisabled();
|
||
});
|
||
|
||
test("test enabled with host_alias alone (no explicit host)", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
await page.fillForm({ name: "alias-only", hostAlias: "prod" });
|
||
await expect(page.testButton).toBeEnabled();
|
||
});
|
||
|
||
test("identity_source=agent hides the identity file field; switching to file reveals it", async ({
|
||
testPage,
|
||
}) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
// Default is "agent" — file input hidden.
|
||
await expect(page.identityFileInput).toBeHidden();
|
||
|
||
await page.fillForm({ identitySource: "file" });
|
||
await expect(page.identityFileInput).toBeVisible();
|
||
|
||
await page.fillForm({ identitySource: "agent" });
|
||
await expect(page.identityFileInput).toBeHidden();
|
||
});
|
||
|
||
test("default port is 22", async ({ testPage }) => {
|
||
const page = new SSHSettingsPage(testPage);
|
||
await page.gotoNew();
|
||
await expect(page.portInput).toHaveValue("22");
|
||
});
|
||
});
|