一键导入
testing
Run and write tests for the Shorted project. Use when running tests, writing unit tests, integration tests, or E2E tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Run and write tests for the Shorted project. Use when running tests, writing unit tests, integration tests, or E2E tests.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Diagnose and fix Shorted web performance (Core Web Vitals, bundle size, Lighthouse/PageSpeed) and keep it from regressing. Use when a PageSpeed/Lighthouse report comes in, LCP/TBT/CLS is high, the JS bundle grows, images are heavy, or someone asks to "make the site faster" / "improve the score".
Operate and improve the Shorted weekly/monthly/yearly short-selling report pipeline — run the generator, iterate on prompts safely, debug quality-gate failures, extend the data snapshot, and keep the generator↔proto JSON contract intact. Use when generating/regenerating reports, tuning report prompts, adding fields to the report snapshot, or debugging /reports pages.
Operate and improve the Shorted investigative newsroom — generate, validate, publish, and iterate on MDX editorial takes. Use when running newsroom-daily/preview, regenerating images, fixing article quality, extending the MDX component palette, or debugging duplicates in the wire feed.
Compose and post full-stack social posts to @shorted___ on X — text + X-card link preview + generated infographic. Wraps the Twitter bot, /api/og/twitter PNG endpoint, and /news/[slug] editorial pages. Use when the user says "post a tweet", "tweet today's shorts", "share to X", "social post", "compose a tweet about $TICKER", or "/social-post".
Add new Connect-RPC API endpoints to the shorts backend. Use when creating new API methods, defining protobuf services, or implementing backend handlers.
Manage data population and synchronization for ASIC short data and stock prices. Use when populating the database, syncing data, or troubleshooting data issues.
| name | testing |
| description | Run and write tests for the Shorted project. Use when running tests, writing unit tests, integration tests, or E2E tests. |
| allowed-tools | Read, Write, Bash(make:*), Bash(go:*), Bash(npm:*), Bash(npx:*), Grep, Glob |
This skill helps you run and write tests for the Shorted project.
# Run all tests (recommended before pushing)
make test
# Frontend tests only
make test-frontend
# Backend tests only
make test-backend
# Integration tests (requires Docker)
make test-integration-local
# E2E tests
cd web && npm run test:e2e
shorted/
├── web/
│ ├── src/@/components/ui/__tests__/ # Component unit tests
│ ├── src/@/lib/__tests__/ # Library unit tests
│ ├── src/__tests__/ # App-level tests
│ └── e2e/ # Playwright E2E tests
├── services/
│ ├── shorts/internal/services/shorts/ # Service tests (*_test.go)
│ ├── shorts/internal/store/shorts/ # Store tests (*_test.go)
│ └── test/integration/ # Integration tests
└── test/
└── integration/ # Full-stack integration tests
// services/shorts/internal/services/shorts/service_test.go
package shorts
import (
"context"
"testing"
"connectrpc.com/connect"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"
shortsv1alpha1 "github.com/castlemilk/shorted.com.au/services/gen/proto/go/shorts/v1alpha1"
"github.com/castlemilk/shorted.com.au/services/shorts/internal/store/shorts/mocks"
"github.com/castlemilk/shorted.com.au/services/pkg/log"
)
func TestGetStock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create mock store
mockStore := mocks.NewMockStore(ctrl)
// Set up expectations
expectedStock := &stockv1alpha1.Stock{
ProductCode: "BHP",
Name: "BHP Group Limited",
}
mockStore.EXPECT().
GetStock("BHP").
Return(expectedStock, nil)
// Create service with mock
svc := NewShortsService(mockStore, log.NewLogger())
// Execute
resp, err := svc.GetStock(context.Background(),
connect.NewRequest(&shortsv1alpha1.GetStockRequest{
ProductCode: "BHP",
}))
// Assert
require.NoError(t, err)
assert.Equal(t, "BHP", resp.Msg.Stock.ProductCode)
assert.Equal(t, "BHP Group Limited", resp.Msg.Stock.Name)
}
func TestGetStock_NotFound(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := mocks.NewMockStore(ctrl)
mockStore.EXPECT().
GetStock("INVALID").
Return(nil, errors.New("stock not found"))
svc := NewShortsService(mockStore, log.NewLogger())
_, err := svc.GetStock(context.Background(),
connect.NewRequest(&shortsv1alpha1.GetStockRequest{
ProductCode: "INVALID",
}))
require.Error(t, err)
assert.Equal(t, connect.CodeNotFound, connect.CodeOf(err))
}
func TestGetTopShorts(t *testing.T) {
tests := []struct {
name string
period string
limit int32
wantLen int
wantError bool
}{
{
name: "1 month period",
period: "1m",
limit: 10,
wantLen: 10,
},
{
name: "3 month period",
period: "3m",
limit: 5,
wantLen: 5,
},
{
name: "invalid period",
period: "invalid",
limit: 10,
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockStore := mocks.NewMockStore(ctrl)
// Set up expectations based on test case...
svc := NewShortsService(mockStore, log.NewLogger())
resp, err := svc.GetTopShorts(context.Background(),
connect.NewRequest(&shortsv1alpha1.GetTopShortsRequest{
Period: tt.period,
Limit: tt.limit,
}))
if tt.wantError {
require.Error(t, err)
return
}
require.NoError(t, err)
assert.Len(t, resp.Msg.Stocks, tt.wantLen)
})
}
}
# Install mockgen (if not installed)
go install go.uber.org/mock/mockgen@latest
# Generate mocks for a store interface
mockgen -source=services/shorts/internal/store/shorts/store.go \
-destination=services/shorts/internal/store/shorts/mocks/mock_store.go \
-package=mocks
# All backend tests
make test-backend
# Specific package
cd services && go test -v ./shorts/internal/services/shorts/...
# With coverage
cd services && go test -v -coverprofile=coverage.out ./...
cd services && go tool cover -html=coverage.out -o coverage.html
// web/src/@/components/ui/__tests__/sparkline.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { Sparkline, SparklineData } from "../sparkline";
const mockData: SparklineData[] = [
{ date: new Date("2024-01-01"), value: 100 },
{ date: new Date("2024-01-02"), value: 105 },
{ date: new Date("2024-01-03"), value: 102 },
];
describe("Sparkline", () => {
it("renders without crashing", () => {
render(<Sparkline data={mockData} width={200} height={50} />);
expect(document.querySelector("svg")).toBeInTheDocument();
});
it("returns null for insufficient data", () => {
const { container } = render(
<Sparkline data={[mockData[0]!]} width={200} height={50} />
);
expect(container.firstChild).toBeNull();
});
it("uses correct color for positive trend", () => {
render(<Sparkline data={mockData} width={200} height={50} isPositive />);
// Check for emerald color class or style
});
it("uses correct color for negative trend", () => {
render(
<Sparkline data={mockData} width={200} height={50} isPositive={false} />
);
// Check for red color class or style
});
it("calls onHover callback", () => {
const onHover = jest.fn();
render(
<Sparkline data={mockData} width={200} height={50} onHover={onHover} />
);
const svg = document.querySelector("svg")!;
fireEvent.mouseMove(svg);
expect(onHover).toHaveBeenCalled();
});
});
// web/src/@/hooks/__tests__/use-stock-data.test.ts
import { renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { useStockData } from "../use-stock-data";
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
return ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
describe("useStockData", () => {
it("fetches stock data successfully", async () => {
const { result } = renderHook(() => useStockData("BHP"), {
wrapper: createWrapper(),
});
expect(result.current.isLoading).toBe(true);
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
});
expect(result.current.data).toBeDefined();
expect(result.current.data?.productCode).toBe("BHP");
});
});
# All frontend tests
make test-frontend
# Watch mode
cd web && npm run test:watch
# With coverage
cd web && npm run test:coverage
# Specific test file
cd web && npm test -- sparkline.test.tsx
Integration tests use testcontainers to spin up real PostgreSQL:
// test/integration/topshorts_test.go
func TestGetTopShortsIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// Setup testcontainer
ctx := context.Background()
pgContainer, err := postgres.RunContainer(ctx,
testcontainers.WithImage("postgres:15"),
postgres.WithDatabase("shorts"),
postgres.WithUsername("test"),
postgres.WithPassword("test"),
)
require.NoError(t, err)
defer pgContainer.Terminate(ctx)
// Get connection string
connStr, err := pgContainer.ConnectionString(ctx)
require.NoError(t, err)
// Run migrations and seed data...
// Test actual API calls
}
Run integration tests:
# Requires Docker
make test-integration-local
// web/e2e/homepage.spec.ts
import { test, expect } from "@playwright/test";
test.describe("Homepage", () => {
test("displays top shorted stocks", async ({ page }) => {
await page.goto("/");
// Wait for data to load
await expect(page.getByTestId("top-shorts-list")).toBeVisible();
// Check that stocks are displayed
const stockItems = page.getByTestId("stock-item");
await expect(stockItems).toHaveCount(10);
});
test("navigates to stock detail page", async ({ page }) => {
await page.goto("/");
// Click on first stock
await page.getByTestId("stock-item").first().click();
// Verify navigation
await expect(page).toHaveURL(/\/stocks\/.+/);
await expect(page.getByTestId("stock-header")).toBeVisible();
});
});
# Run all E2E tests
cd web && npm run test:e2e
# Run with UI
cd web && npm run test:e2e:ui
# Debug mode
cd web && npm run test:e2e:debug
# View report
cd web && npm run test:e2e:report
Always run the full test suite before pushing:
make test
This runs: