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
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
# batch of tooling bumps, and the two workspaces that hold their own manifests
# are covered explicitly, since Dependabot does not follow pnpm workspaces from
# the root.
#
# Every update targets `staging`, not `main`. Work reaches production through
# staging in this repository, and a dependency bump is not an exception: a PR
# opened against `main` cannot be merged by the normal flow, so the nine that
# were open had simply accumulated there unmergeable.
version: 2

updates:
- package-ecosystem: npm
target-branch: staging
directory: /
schedule:
interval: weekly
Expand All @@ -27,6 +33,7 @@ updates:
labels: [dependencies, security]

- package-ecosystem: npm
target-branch: staging
directory: /web
schedule:
interval: weekly
Expand All @@ -40,6 +47,7 @@ updates:
labels: [dependencies, frontend]

- package-ecosystem: npm
target-branch: staging
directory: /docs
schedule:
interval: weekly
Expand All @@ -55,6 +63,7 @@ updates:
# Workflow actions pin to tags that go stale and are a supply-chain surface in
# their own right.
- package-ecosystem: github-actions
target-branch: staging
directory: /
schedule:
interval: weekly
Expand Down
185 changes: 92 additions & 93 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -224,102 +224,101 @@ jobs:
echo "All compliance tests must pass (100% required for ATProto data sovereignty)." >> $GITHUB_STEP_SUMMARY

# ==============================================================================
# Stage 5: E2E Tests (DISABLED - re-enable when infrastructure is ready)
# Stage 5: E2E Tests
# ==============================================================================

# e2e-tests:
# name: E2E Tests (${{ matrix.browser }})
# runs-on: ubuntu-latest
# needs: [integration-tests]
# strategy:
# fail-fast: false
# matrix:
# browser: ${{ github.event_name == 'pull_request' && fromJSON('["chromium"]') || fromJSON('["chromium", "firefox", "webkit"]') }}
#
# services:
# postgres:
# image: postgres:16-alpine
# env:
# POSTGRES_DB: chive_test
# POSTGRES_USER: chive
# POSTGRES_PASSWORD: chive_test_password
# ports:
# - 5432:5432
# options: >-
# --health-cmd pg_isready
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
#
# redis:
# image: redis:7-alpine
# ports:
# - 6379:6379
# options: >-
# --health-cmd "redis-cli ping"
# --health-interval 10s
# --health-timeout 5s
# --health-retries 5
#
# env:
# POSTGRES_HOST: localhost
# POSTGRES_PORT: 5432
# POSTGRES_USER: chive
# POSTGRES_PASSWORD: chive_test_password
# POSTGRES_DB: chive_test
# DATABASE_URL: postgresql://chive:chive_test_password@localhost:5432/chive_test
# REDIS_URL: redis://localhost:6379
# ELASTICSEARCH_URL: http://localhost:9200
# NEO4J_URI: bolt://localhost:7687
# NEO4J_USER: neo4j
# NEO4J_PASSWORD: chive_test_password
# DISABLE_RATE_LIMITING: 'true'
#
# steps:
# - uses: actions/checkout@v4
# - uses: ./.github/actions/setup-node-pnpm
# - uses: ./.github/actions/start-services
#
# - name: Cache Playwright browsers
# uses: actions/cache@v4
# id: playwright-cache
# with:
# path: ~/.cache/ms-playwright
# key: playwright-${{ runner.os }}-${{ matrix.browser }}-${{ hashFiles('**/pnpm-lock.yaml') }}
#
# - name: Install Playwright browsers
# if: steps.playwright-cache.outputs.cache-hit != 'true'
# run: |
# # Always install chromium (needed for setup:auth project)
# pnpm exec playwright install --with-deps chromium
# # Install matrix browser if different from chromium
# if [ "${{ matrix.browser }}" != "chromium" ]; then
# pnpm exec playwright install --with-deps ${{ matrix.browser }}
# fi
#
# - name: Install Playwright dependencies (cached)
# if: steps.playwright-cache.outputs.cache-hit == 'true'
# run: |
# pnpm exec playwright install-deps chromium
# if [ "${{ matrix.browser }}" != "chromium" ]; then
# pnpm exec playwright install-deps ${{ matrix.browser }}
# fi
#
# - name: Run database migrations
# run: pnpm db:migrate:up
#
# - name: Run E2E tests
# run: pnpm test:e2e --project="${{ matrix.browser }}:*"
# Only the unauthenticated project runs here. It covers the sign-in and
# landing pages — 24 tests, about two minutes — and is green.
#
# - name: Upload Playwright report
# if: always()
# uses: actions/upload-artifact@v4
# with:
# name: playwright-report-${{ matrix.browser }}
# path: |
# playwright-report/
# tests/e2e/test-results/
# retention-days: 14
# The authenticated project is 487 tests and takes hours on one worker, which
# is too slow to gate every pull request and has not been triaged end to end.
# Enabling it needs its own pass: a decision about sharding, and a review of
# what currently fails. Running the part that is known good is worth more
# than running nothing, which is what happened while the whole job sat
# commented out.

e2e-tests:
name: E2E Tests (unauthenticated)
runs-on: ubuntu-latest
needs: [integration-tests]

services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: chive_test
POSTGRES_USER: chive
POSTGRES_PASSWORD: chive_test_password
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5

redis:
image: redis:7-alpine
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5

env:
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_USER: chive
POSTGRES_PASSWORD: chive_test_password
POSTGRES_DB: chive_test
DATABASE_URL: postgresql://chive:chive_test_password@localhost:5432/chive_test
PGHOST: localhost
PGPORT: 5432
PGUSER: chive
PGPASSWORD: chive_test_password
PGDATABASE: chive_test
REDIS_URL: redis://localhost:6379
ELASTICSEARCH_URL: http://localhost:9200
NEO4J_URI: bolt://localhost:7687
NEO4J_USER: neo4j
NEO4J_PASSWORD: chive_test_password
ADMIN_DIDS: did:plc:test-admin-did
DISABLE_RATE_LIMITING: 'true'

steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-node-pnpm
- uses: ./.github/actions/start-services

- name: Cache Playwright browsers
uses: actions/cache@v4
id: playwright-cache
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-chromium-${{ hashFiles('**/pnpm-lock.yaml') }}

- name: Install Playwright browser
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: pnpm exec playwright install --with-deps chromium

- name: Install Playwright system dependencies
if: steps.playwright-cache.outputs.cache-hit == 'true'
run: pnpm exec playwright install-deps chromium

- name: Run E2E tests
run: pnpm test:e2e --project="chromium:unauthenticated"

- name: Upload Playwright report
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: |
playwright-report/
tests/e2e/test-results/
retention-days: 14

# ==============================================================================
# Stage 6: Build Verification
Expand Down
24 changes: 19 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,13 @@ cd chive
# Install dependencies
pnpm install

# Generate the lexicon types.
#
# `src/lexicons/generated/` and `web/lib/api/generated/` are gitignored and
# built from `lexicons/`, so on a fresh clone they do not exist yet and
# `pnpm typecheck` fails with "Cannot find module" until this has run.
pnpm lexicons:generate

# Start the development database stack
./scripts/start-test-stack.sh

Expand Down Expand Up @@ -192,12 +199,19 @@ pnpm db:migrate:down

Chive uses a hybrid plugin architecture with dependency injection (TSyringe) and event hooks (EventEmitter2).

Plugins run in isolated sandboxes with declared permissions. Built-in plugins include:
Plugins run in isolated sandboxes with declared permissions. The plugins the
running services register today are:

- arXiv, PsyArXiv, LingBuzz and Semantics Archive preprint metadata
- OpenReview review metadata
- Cosmik backlinks, connections, follows and link removals
- Margin notes and replies

- GitHub integration
- ORCID linking
- DOI registration
- Wikidata field import
`src/plugins/builtin/` holds more than are registered — GitHub, ORCID, DOI
registration, Wikidata, Crossref, OpenAlex, ROR, Zenodo, Figshare, Dryad, OSF,
Software Heritage and others are written but not constructed by any service.
This list names the ones that actually run; the others are wired up as the
integrations they belong to are finished.

See [docs.chive.pub](https://docs.chive.pub) for plugin development details.

Expand Down
12 changes: 7 additions & 5 deletions tests/e2e/auth.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,11 +157,13 @@ test.describe('Authentication', () => {
const signInPage = new SignInPage(page);
await signInPage.goto();

// Form should have a visible label
const label = page.getByText('Handle or DID');
await expect(label).toBeVisible();

// Input should be visible and focusable
// Assert the association, not merely that the words appear somewhere:
// `getByLabel` only matches when the label is actually bound to the
// control, which is the property a screen reader depends on. The old
// `getByText('Handle or DID')` matched the label and the description
// both, and would have passed even while the input had no accessible
// name at all — which is exactly the state the page was in.
await expect(page.getByLabel('Handle or DID')).toBeVisible();
await expect(signInPage.handleInput).toBeVisible();

// Button should be focusable
Expand Down
49 changes: 23 additions & 26 deletions tests/e2e/eprint/author-display.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,14 @@
.or(page.locator('[data-testid="corresponding-badge"]'))
.or(page.getByRole('img', { name: /corresponding/i }));

// May or may not be present depending on data
const isVisible = await correspondingBadge.isVisible({ timeout: 3000 }).catch(() => false);
// Just verify page loaded, badge is optional
expect(true).toBe(true);
// scripts/seed-test-data.ts sets isCorrespondingAuthor on the sole author
// of every seeded eprint, so this indicator is not optional and the test
// can assert it. It used to compute the visibility and discard the answer,
// so it passed whether or not the badge rendered.
await expect(correspondingBadge.first()).toBeVisible();
});

test('displays highlighted author indicator (co-first)', async ({ page }) => {
test('shows no highlighted-author indicator when no author is highlighted', async ({ page }) => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();

// Look for highlighted author indicator
Expand All @@ -135,10 +136,11 @@
.or(page.locator('[data-testid="highlighted-badge"]'))
.or(page.getByText(/\u2020/)); // Dagger symbol

// May or may not be present depending on data
const isVisible = await highlightedBadge.isVisible({ timeout: 3000 }).catch(() => false);
// Just verify page loaded, badge is optional
expect(true).toBe(true);
// The seed sets isHighlighted false on every author, so absence is what
// this page can assert — and it is worth asserting: a bug rendering the
// dagger unconditionally would mark every author co-first, which the
// previous form of this test could not have noticed.
await expect(highlightedBadge.first()).toBeHidden();
});

test('displays ORCID link when available', async ({ page }) => {
Expand Down Expand Up @@ -179,27 +181,23 @@

// Look for contribution types
const contributions = page.getByText(/conceptualization|methodology|investigation|writing/i);
const isVisible = await contributions.isVisible({ timeout: 3000 }).catch(() => false);
// Just verify expansion works, contributions are optional
expect(true).toBe(true);
// The seeded authors carry an empty contributions array, so no CRediT
// role should appear. That catches an expansion rendering placeholders.
await expect(contributions.first()).toBeHidden();
}
});

test('displays contribution degree when available', async ({ page }) => {
test('shows no contribution degree when the author has no contributions', async ({ page }) => {
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();

// Look for contribution degrees (lead/equal/supporting)
const degrees = page.getByText(/lead|equal|supporting/i).filter({
has: page.locator('[data-testid="contribution-degree"]').or(page.locator('.contribution')),
});

// May or may not be visible depending on data structure
const isVisible = await degrees
.first()
.isVisible({ timeout: 3000 })
.catch(() => false);
// Just verify page loads, degrees are optional
expect(true).toBe(true);
// Same reasoning: with no contributions seeded, a degree chip appearing
// would be the page inventing data.
await expect(degrees.first()).toBeHidden();
});
});

Expand All @@ -212,10 +210,9 @@
// External authors (if any) should not have clickable links
// This is tested implicitly - if no profile link, clicking does nothing
const authorName = page.getByText(SEEDED_AUTHORS.white.displayName);
// The assertion above is the whole test: the seeded author renders by
// name. The trailing expect(true) added nothing.
await expect(authorName).toBeVisible({ timeout: 10000 });

// Just verify page displays correctly
expect(true).toBe(true);
});

test('external authors can show ORCID even without DID', async ({ page }) => {
Expand All @@ -227,9 +224,9 @@
.getByRole('link', { name: /orcid/i })
.or(page.locator('a[href*="orcid.org"]'));

// Optional - may or may not have external authors with ORCID
const isVisible = await orcidLink.isVisible({ timeout: 3000 }).catch(() => false);
expect(true).toBe(true);
// The seed gives this author an ORCID, so the link is not optional here.
await expect(orcidLink.first()).toBeVisible();
await expect(orcidLink.first()).toHaveAttribute('href', /orcid\.org/);
});
});

Expand Down
Loading
Loading