Files
agent/.superpowers/sdd/task-1-brief.md
2026-07-20 08:02:20 +08:00

5.9 KiB

Task 1: Login Page And App-Level Session

Files:

  • Modify: apps/web/src/features/auth/ServerLogin.svelte
  • Modify: apps/web/src/app/App.svelte
  • Test: apps/web/src/features/auth/ServerLogin.test.ts

Interfaces:

  • Produces: ServerLogin Svelte component event login: { apiBase: string; account: string }

  • Produces: app state fields apiBase: string, currentUser: { account: string } | null

  • Consumes: localStorage['apiBase']

  • Step 1: Write failing login component tests

Create apps/web/src/features/auth/ServerLogin.test.ts:

import '@testing-library/jest-dom/vitest';
import { fireEvent, render, screen } from '@testing-library/svelte';
import { describe, expect, it, vi } from 'vitest';
import ServerLogin from './ServerLogin.svelte';

describe('ServerLogin', () => {
  it('renders server, account, and password fields', () => {
    render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });

    expect(screen.getByLabelText('Server IP or domain')).toBeInTheDocument();
    expect(screen.getByLabelText('Email or username')).toBeInTheDocument();
    expect(screen.getByLabelText('Password')).toBeInTheDocument();
    expect(screen.getByRole('button', { name: 'Log in' })).toBeInTheDocument();
  });

  it('normalizes server address and emits login details', async () => {
    const onLogin = vi.fn();
    render(ServerLogin, {
      props: {
        apiBase: 'localhost:8080',
        onLogin,
      },
    });

    await fireEvent.input(screen.getByLabelText('Server IP or domain'), {
      target: { value: '10.0.0.12:8080' },
    });
    await fireEvent.input(screen.getByLabelText('Email or username'), {
      target: { value: 'david@example.com' },
    });
    await fireEvent.input(screen.getByLabelText('Password'), {
      target: { value: 'secret' },
    });
    await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));

    expect(onLogin).toHaveBeenCalledWith({
      apiBase: 'http://10.0.0.12:8080',
      account: 'david@example.com',
    });
  });

  it('shows an error when account or password is missing', async () => {
    render(ServerLogin, { props: { apiBase: 'http://localhost:8080' } });

    await fireEvent.click(screen.getByRole('button', { name: 'Log in' }));

    expect(screen.getByText('Enter an account and password.')).toBeInTheDocument();
  });
});
  • Step 2: Run the new test to verify it fails

Run:

Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/auth/ServerLogin.test.ts

Expected: FAIL because ServerLogin does not render account/password fields and does not expose onLogin.

  • Step 3: Replace ServerLogin.svelte with the complete login page

Implement apps/web/src/features/auth/ServerLogin.svelte:

<script lang="ts">
  export let apiBase: string;
  export let onLogin: (detail: { apiBase: string; account: string }) => void = () => {};

  let server = apiBase;
  let account = '';
  let password = '';
  let error = '';

  function normalizeServer(value: string) {
    const trimmed = value.trim();
    if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
      return trimmed;
    }
    return `http://${trimmed}`;
  }

  function submitLogin() {
    error = '';
    if (!account.trim() || !password.trim()) {
      error = 'Enter an account and password.';
      return;
    }

    const normalized = normalizeServer(server);
    server = normalized;
    onLogin({ apiBase: normalized, account: account.trim() });
  }
</script>

<main class="login-page">
  <section class="login-panel" aria-labelledby="login-title">
    <div class="login-brand" aria-hidden="true">SA</div>
    <div class="login-heading">
      <p>Private workbench</p>
      <h1 id="login-title">SenlinAI Workbench</h1>
    </div>

    <form class="server-login" aria-label="Server login" on:submit|preventDefault={submitLogin}>
      <label for="server-address">Server IP or domain</label>
      <input id="server-address" name="server" bind:value={server} placeholder="http://localhost:8080" />

      <label for="login-account">Email or username</label>
      <input id="login-account" name="account" bind:value={account} autocomplete="username" />

      <label for="login-password">Password</label>
      <input
        id="login-password"
        name="password"
        type="password"
        bind:value={password}
        autocomplete="current-password"
      />

      {#if error}
        <p class="form-error" aria-live="polite">{error}</p>
      {/if}

      <button type="submit">Log in</button>
      <p class="login-note">The server address is remembered on this device.</p>
    </form>
  </section>
</main>
  • Step 4: Update App.svelte to switch from login to workbench

Temporarily render a simple logged-in placeholder; the full workbench arrives in Task 3.

<script lang="ts">
  import ServerLogin from '../features/auth/ServerLogin.svelte';

  let apiBase = localStorage.getItem('apiBase') ?? 'http://localhost:8080';
  let currentUser: { account: string } | null = null;

  function handleLogin(detail: { apiBase: string; account: string }) {
    apiBase = detail.apiBase;
    localStorage.setItem('apiBase', apiBase);
    currentUser = { account: detail.account };
  }
</script>

{#if currentUser}
  <main class="workbench-placeholder" aria-label="Project workbench">
    <h1>SenlinAI Workbench</h1>
    <p>Signed in as {currentUser.account}</p>
  </main>
{:else}
  <ServerLogin {apiBase} onLogin={handleLogin} />
{/if}
  • Step 5: Run tests

Run:

Set-Location D:\work\senlinai\agent\apps\web
npm test -- --run src/features/auth/ServerLogin.test.ts

Expected: PASS.

  • Step 6: Commit

Run:

Set-Location D:\work\senlinai\agent
git add apps\web\src\features\auth\ServerLogin.svelte apps\web\src\features\auth\ServerLogin.test.ts apps\web\src\app\App.svelte
git commit -m "feat: add full workbench login page"