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
2 changes: 1 addition & 1 deletion .claude/skills/git/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ description: Git 操作ルール
- ドメイン: `example.com` / `example.org` / `example.net`、`*.example` / `*.test` / `*.invalid` / `*.localhost`(RFC 2606 / RFC 6761)
- IP: `127.0.0.1`、TEST-NET(`192.0.2.0/24`, `198.51.100.0/24`, `203.0.113.0/24`)、`2001:db8::/32`
- メール: `user@example.com` 系
- E2E の `localhost:8010` / `127.0.0.1` は正当(test-server / 外部リンクシミュレーション)
- E2E の `localhost:<動的ポート>` / `127.0.0.1` は正当(test-server / 外部リンクシミュレーション)

**検出対象(混入してはいけない値):**

Expand Down
4 changes: 2 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
| `@nitpicker/analyze-*` | axe / lighthouse / markuplint / textlint / search の各監査 |
| `@nitpicker/report-google-sheets` | Google Sheets レポート出力(5 フェーズの `createSheets`) |
| `@nitpicker/types` | 監査型定義(Report / ConfigJSON) |
| `packages/test-server` | E2E 用 Hono サーバー(port 8010、プロダクション非依存) |
| `packages/test-server` | E2E 用 Hono サーバー(OS割り当ての動的ポート、プロダクション非依存) |

## 境界と所有権

Expand Down Expand Up @@ -169,6 +169,6 @@
## テストと CLI 契約

- ユニット: `yarn test`(Vitest 4、1関数1ファイルにユニットテスト必須)
- E2E: `yarn vitest run --config vitest.e2e.config.ts`(maxWorkers: 1、test-server は port 8010、**外部リンクは `127.0.0.1` でシミュレート**(`localhost` と別ホスト名扱い))
- E2E: `yarn vitest run --config vitest.e2e.config.ts`(maxWorkers: 1、test-server はOS割り当ての動的ポート(並行worktree/セッション間の `EADDRINUSE` を回避、#162)、**外部リンクは `127.0.0.1` でシミュレート**(`localhost` と別ホスト名扱い))
- viewer E2E: `yarn workspace @nitpicker/viewer test:e2e`(Playwright、fixture 生成 → 実 CLI 起動 → ブラウザ検証)
- CLI 終了コード: `0` = 成功 / `1` = 致命的(スコープ内エラー含む)/ `2` = 警告(外部リンクエラーのみ。`--strict` で 1 に昇格)
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ npx @nitpicker/cli viewer-build <archive> [--force] # viewer read model を

```sh
yarn test # ユニットテスト(Vitest)
yarn vitest run --config vitest.e2e.config.ts # E2E(maxWorkers: 1、test-server port 8010
yarn vitest run --config vitest.e2e.config.ts # E2E(maxWorkers: 1、test-server はOS割り当ての動的ポート
yarn workspace @nitpicker/viewer test:e2e # viewer の Playwright E2E
yarn build # 全パッケージビルド
yarn lint # lint + prettier + cspell
Expand Down
4 changes: 2 additions & 2 deletions packages/@nitpicker/crawler/src/crawler/crawler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1569,8 +1569,8 @@ export default class Crawler extends EventEmitter<CrawlerEventTypes> {
// sub-resource requests issued from the same page —
// including cross-origin requests to a different hostname
// sharing the same IP / port (e.g. an embedded
// `<img src="http://127.0.0.1:8010/…">` loaded from a
// `localhost:8010` page) — get the cached `Authorization`
// `<img src="http://127.0.0.1:PORT/…">` loaded from a
// `localhost:PORT` page) — get the cached `Authorization`
// header re-attached by the network stack. The
// `Fetch.authRequired` event never fires for these
// pre-emptive attachments, so neither `page.authenticate`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Report } from '@nitpicker/types';

import { Sheets } from '@d-zero/google-sheets';
import { google } from 'googleapis';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vitest';
import { afterAll, afterEach, beforeAll, describe, expect, inject, it } from 'vitest';

import { createLinks } from '../../data/create-links.js';
import { createPageList } from '../../data/create-page-list.js';
Expand All @@ -23,6 +23,16 @@ import {
testSheetName,
} from './helpers.js';

// This `ProvidedContext` shape is duplicated in
// `packages/test-server/src/__tests__/e2e/test-server-port.ts` (a separate TS
// project — this package has no reference to that one) — keep both
// declarations in sync if `testServerPort`'s shape ever changes.
declare module 'vitest' {
interface ProvidedContext {
testServerPort: number;
}
}

describe('createSheets pipeline', () => {
let auth: Auth;
let crawlResult: CrawlResult;
Expand All @@ -31,7 +41,8 @@ describe('createSheets pipeline', () => {

beforeAll(async () => {
auth = await getAuth();
crawlResult = await crawlTestServer(['http://localhost:8010/'], {
const port = inject('testServerPort');
crawlResult = await crawlTestServer([`http://localhost:${port}/`], {
recursive: true,
fetchExternal: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import { Archive, CrawlerOrchestrator } from '@nitpicker/crawler';
import { getViolations } from '@nitpicker/query';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { TEST_SERVER_PORT } from './test-server-port.js';

/**
* crawl → write → analyze のクロスパッケージ統合テスト。
*
Expand All @@ -29,7 +31,7 @@ describe('Analyze pipeline (crawl → write → analyze)', () => {

// 1) クロールして .nitpicker を書き出す(スナップショットは SQLite BLOB として保存される)
const orchestrator = await CrawlerOrchestrator.crawling(
['http://localhost:8010/meta/'],
[`http://localhost:${TEST_SERVER_PORT}/meta/`],
{
cwd,
interval: 0,
Expand Down Expand Up @@ -69,6 +71,6 @@ describe('Analyze pipeline (crawl → write → analyze)', () => {
// /meta/ 配下の internal ページの HTML が WorkerPool に渡り、
// per-URL のデータが生成されている(HTML が読めなければ 0 件になる)
expect(analyzedUrls.length).toBeGreaterThan(0);
expect(analyzedUrls).toContain('http://localhost:8010/meta/full');
expect(analyzedUrls).toContain(`http://localhost:${TEST_SERVER_PORT}/meta/full`);
});
});
20 changes: 11 additions & 9 deletions packages/test-server/src/__tests__/e2e/append.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';
import { Archive, CrawlerOrchestrator } from '@nitpicker/crawler';
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';

import { TEST_SERVER_ORIGIN } from './test-server-port.js';

/**
* Run a baseline crawl that creates a `.nitpicker` archive on disk, then close
* it so the caller can re-open it via `Archive.open`. Returns the archive file
Expand Down Expand Up @@ -44,7 +46,7 @@ describe('Append crawl', () => {
beforeAll(async () => {
// 1) Baseline archive scoped to /scope/blog/. /scope/docs/ is reached via
// a link and therefore recorded as external metadata-only.
const baseline = await crawlAndPersist(['http://localhost:8010/scope/blog/'], {
const baseline = await crawlAndPersist([`${TEST_SERVER_ORIGIN}/scope/blog/`], {
fetchExternal: true,
});
filePath = baseline.filePath;
Expand All @@ -54,7 +56,7 @@ describe('Append crawl', () => {
// previously-external /scope/docs/ and re-crawls it as internal.
const orchestrator = await CrawlerOrchestrator.append(
filePath,
['http://localhost:8010/scope/docs/'],
[`${TEST_SERVER_ORIGIN}/scope/docs/`],
{ cwd, fetchExternal: true },
);
await orchestrator.write();
Expand Down Expand Up @@ -87,8 +89,8 @@ describe('Append crawl', () => {
it('info.roots records both the original and the appended root', async () => {
const config = await accessor.getConfig();
expect(config.roots).toEqual([
'http://localhost:8010/scope/blog/',
'http://localhost:8010/scope/docs/',
`${TEST_SERVER_ORIGIN}/scope/blog/`,
`${TEST_SERVER_ORIGIN}/scope/docs/`,
]);
});

Expand Down Expand Up @@ -119,7 +121,7 @@ describe('Append crawl: restore from .bak on failure', () => {
let originalArchiveBytes: Buffer;

beforeAll(async () => {
const baseline = await crawlAndPersist(['http://localhost:8010/scope/blog/']);
const baseline = await crawlAndPersist([`${TEST_SERVER_ORIGIN}/scope/blog/`]);
filePath = baseline.filePath;
cwd = baseline.cwd;
originalArchiveBytes = await fs.readFile(filePath);
Expand All @@ -142,7 +144,7 @@ describe('Append crawl: restore from .bak on failure', () => {
.mockRejectedValueOnce(new Error('forced-repromote-failure'));

await expect(
CrawlerOrchestrator.append(filePath, ['http://localhost:8010/scope/docs/'], {
CrawlerOrchestrator.append(filePath, [`${TEST_SERVER_ORIGIN}/scope/docs/`], {
cwd,
}),
).rejects.toThrow(/forced-repromote-failure/);
Expand Down Expand Up @@ -193,7 +195,7 @@ describe('Append crawl: restore from .bak on failure', () => {

const thrown = await CrawlerOrchestrator.append(
filePath,
['http://localhost:8010/scope/docs/'],
[`${TEST_SERVER_ORIGIN}/scope/docs/`],
{ cwd },
).then(
() => {
Expand Down Expand Up @@ -223,7 +225,7 @@ describe('Append crawl: list-mode rejection', () => {
let cwd: string;

beforeAll(async () => {
const baseline = await crawlAndPersist(['http://localhost:8010/scope/blog/']);
const baseline = await crawlAndPersist([`${TEST_SERVER_ORIGIN}/scope/blog/`]);
filePath = baseline.filePath;
cwd = baseline.cwd;

Expand All @@ -243,7 +245,7 @@ describe('Append crawl: list-mode rejection', () => {

it('throws a helpful error when appending to a list-mode archive', async () => {
await expect(
CrawlerOrchestrator.append(filePath, ['http://localhost:8010/scope/docs/'], {
CrawlerOrchestrator.append(filePath, [`${TEST_SERVER_ORIGIN}/scope/docs/`], {
cwd,
}),
).rejects.toThrow(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';
import { Archive, CrawlerOrchestrator } from '@nitpicker/crawler';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { TEST_SERVER_PORT } from './test-server-port.js';

describe('Archive pipeline (.nitpicker write → reopen)', () => {
let orchestrator: CrawlerOrchestrator;
let tmpDir: string;
Expand All @@ -17,7 +19,7 @@ describe('Archive pipeline (.nitpicker write → reopen)', () => {
await fs.mkdir(cwd, { recursive: true });

orchestrator = await CrawlerOrchestrator.crawling(
['http://localhost:8010/'],
[`http://localhost:${TEST_SERVER_PORT}/`],
{
cwd,
recursive: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import path from 'node:path';

import { describe, expect, it } from 'vitest';

import { TEST_SERVER_PORT } from './test-server-port.js';

/** Absolute path to the built CLI entry point. */
const CLI_BIN = path.resolve(
import.meta.dirname,
Expand All @@ -23,7 +25,7 @@ describe('CLI process termination', () => {
[
CLI_BIN,
'crawl',
'http://localhost:8010/',
`http://localhost:${TEST_SERVER_PORT}/`,
'--silent',
'--no-image',
'--no-fetch-external',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import type { Config } from '@nitpicker/crawler';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { type CrawlResult, cleanup, crawl } from './helpers.js';
import { TEST_SERVER_PORT } from './test-server-port.js';

describe('Config persistence', () => {
let result: CrawlResult;

beforeAll(async () => {
result = await crawl(['http://localhost:8010/'], {
result = await crawl([`http://localhost:${TEST_SERVER_PORT}/`], {
userAgent: 'NitpickerE2EBot/1.0',
ignoreRobots: true,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { buildViewerReadModel } from '@nitpicker/query';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { type CrawlResult, cleanup, crawl } from './helpers.js';
import { TEST_SERVER_PORT } from './test-server-port.js';

/**
* Legacy flat tables that fresh archives no longer carry — the crawler's
Expand Down Expand Up @@ -49,7 +50,9 @@ describe('crawler write path targets entity tables directly (issue #196)', () =>
// suite asserts on gets at least one row from one crawl. `image: true`
// overrides the helper's default (`false`) so `<img>` elements are
// extracted into `images` / `image_items`.
result = await crawl(['http://localhost:8010/resource-reuse/'], { image: true });
result = await crawl([`http://localhost:${TEST_SERVER_PORT}/resource-reuse/`], {
image: true,
});
}, 120_000);

afterAll(async () => {
Expand Down Expand Up @@ -86,7 +89,7 @@ describe('crawler write path targets entity tables directly (issue #196)', () =>
}[];
expect(viewerPages.length).toBeGreaterThan(0);
expect(viewerPages.map((p) => p.url)).toContain(
'http://localhost:8010/resource-reuse/',
`http://localhost:${TEST_SERVER_PORT}/resource-reuse/`,
);
});

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { type CrawlResult, cleanup, crawl } from './helpers.js';
import { TEST_SERVER_PORT } from './test-server-port.js';

describe('Error status codes', () => {
let result: CrawlResult;

beforeAll(async () => {
result = await crawl(['http://localhost:8010/error-status/']);
result = await crawl([`http://localhost:${TEST_SERVER_PORT}/error-status/`]);
}, 60_000);

afterAll(async () => {
Expand Down
21 changes: 14 additions & 7 deletions packages/test-server/src/__tests__/e2e/exclude.e2e.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import { type CrawlResult, cleanup, crawl } from './helpers.js';
import {
TEST_SERVER_EXTERNAL_ORIGIN,
TEST_SERVER_ORIGIN,
TEST_SERVER_PORT,
} from './test-server-port.js';

describe('Exclude patterns', () => {
describe('パス除外 (excludes)', () => {
let result: CrawlResult;

beforeAll(async () => {
result = await crawl(['http://localhost:8010/exclude/'], {
result = await crawl([`${TEST_SERVER_ORIGIN}/exclude/`], {
excludes: ['/exclude/secret/*'],
});
}, 60_000);
Expand All @@ -34,7 +39,7 @@ describe('Exclude patterns', () => {
let result: CrawlResult;

beforeAll(async () => {
result = await crawl(['http://localhost:8010/exclude/'], {
result = await crawl([`${TEST_SERVER_ORIGIN}/exclude/`], {
excludeKeywords: ['FORBIDDEN_KEYWORD'],
});
}, 60_000);
Expand Down Expand Up @@ -76,8 +81,8 @@ describe('Exclude patterns', () => {
let result: CrawlResult;

beforeAll(async () => {
result = await crawl(['http://localhost:8010/exclude/'], {
excludeUrls: ['http://127.0.0.1:8010/exclude/external-a'],
result = await crawl([`${TEST_SERVER_ORIGIN}/exclude/`], {
excludeUrls: [`${TEST_SERVER_EXTERNAL_ORIGIN}/exclude/external-a`],
});
}, 60_000);

Expand All @@ -89,7 +94,7 @@ describe('Exclude patterns', () => {
const pages = await result.accessor.getPages('external-page');
const urls = pages.map((p) => p.url.href);
const hasExternalA = urls.some((u) =>
u.includes('127.0.0.1:8010/exclude/external-a'),
u.includes(`127.0.0.1:${TEST_SERVER_PORT}/exclude/external-a`),
);
expect(hasExternalA).toBe(false);
});
Expand All @@ -98,14 +103,16 @@ describe('Exclude patterns', () => {
const pages = await result.accessor.getPages('external-page');
const urls = pages.map((p) => p.url.href);
const hasExternalB = urls.some((u) =>
u.includes('127.0.0.1:8010/exclude/external-b'),
u.includes(`127.0.0.1:${TEST_SERVER_PORT}/exclude/external-b`),
);
expect(hasExternalB).toBe(true);
});

it('excludeUrls がアーカイブの config に保存される', async () => {
const config = await result.accessor.getConfig();
expect(config.excludeUrls).toContain('http://127.0.0.1:8010/exclude/external-a');
expect(config.excludeUrls).toContain(
`${TEST_SERVER_EXTERNAL_ORIGIN}/exclude/external-a`,
);
});
});
});
17 changes: 13 additions & 4 deletions packages/test-server/src/__tests__/e2e/global-setup.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import type { Server } from 'node:http';
import type { TestProject } from 'vitest/node';

let server: Server;

/**
*
* Starts the E2E test server on an OS-assigned port and shares the actual
* port with test files via vitest's provide/inject channel — a fixed port
* made concurrent worktrees/sessions collide with `EADDRINUSE` (#162).
* @param project - The vitest project, used to `provide` the resolved port.
*/
export async function setup() {
export async function setup(project: TestProject) {
const { startServer } = await import('../../../src/server.js');
server = await startServer(8010);
console.log('[global-setup] Test server started on port 8010'); // eslint-disable-line no-console
server = await startServer();
const address = server.address();
if (!address || typeof address === 'string') {
throw new Error('[global-setup] test server did not report a TCP port');
}
project.provide('testServerPort', address.port);
console.log(`[global-setup] Test server started on port ${address.port}`); // eslint-disable-line no-console
}

/**
Expand Down
Loading
Loading