fix: remove internal spec doc, add missing e2e tests

This commit is contained in:
nrslib 2026-02-22 08:14:37 +09:00
parent d04f27df79
commit 6a42bc79d1
7 changed files with 364 additions and 1075 deletions

View File

@ -15,8 +15,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- **4-layer facet resolution**: Upgraded from 3-layer (project → user → builtin) to 4-layer (package-local → project → user → builtin) — ensemble package pieces automatically resolve their own facets first - **4-layer facet resolution**: Upgraded from 3-layer (project → user → builtin) to 4-layer (package-local → project → user → builtin) — ensemble package pieces automatically resolve their own facets first
- **Ensemble category in piece selection**: Installed ensemble packages automatically appear as subcategories under an "ensemble" category in the piece selection UI - **Ensemble category in piece selection**: Installed ensemble packages automatically appear as subcategories under an "ensemble" category in the piece selection UI
- **Build gate in implement/fix instructions**: `implement` and `fix` builtin instructions now require build (type check) verification before test execution - **Build gate in implement/fix instructions**: `implement` and `fix` builtin instructions now require build (type check) verification before test execution
- **TAKT Pack specification** (`docs/takt-pack-spec.md`): Documentation for the TAKT package manifest format
### Changed ### Changed
- **BREAKING: Facets directory restructured**: Facet directories moved under a `facets/` subdirectory at all levels — `builtins/{lang}/{facetType}/``builtins/{lang}/facets/{facetType}/`, `~/.takt/{facetType}/``~/.takt/facets/{facetType}/`, `.takt/{facetType}/``.takt/facets/{facetType}/`. Migration: move your custom facet files into the new `facets/` subdirectory - **BREAKING: Facets directory restructured**: Facet directories moved under a `facets/` subdirectory at all levels — `builtins/{lang}/{facetType}/``builtins/{lang}/facets/{facetType}/`, `~/.takt/{facetType}/``~/.takt/facets/{facetType}/`, `.takt/{facetType}/``.takt/facets/{facetType}/`. Migration: move your custom facet files into the new `facets/` subdirectory

View File

@ -250,7 +250,6 @@ await engine.run();
| [Agent Guide](./docs/agents.md) | Custom agent configuration | | [Agent Guide](./docs/agents.md) | Custom agent configuration |
| [Builtin Catalog](./docs/builtin-catalog.md) | All builtin pieces and personas | | [Builtin Catalog](./docs/builtin-catalog.md) | All builtin pieces and personas |
| [Faceted Prompting](./docs/faceted-prompting.md) | Prompt design methodology | | [Faceted Prompting](./docs/faceted-prompting.md) | Prompt design methodology |
| [TAKT Pack Spec](./docs/takt-pack-spec.md) | Ensemble package format |
| [Task Management](./docs/task-management.md) | Task queuing, execution, isolation | | [Task Management](./docs/task-management.md) | Task queuing, execution, isolation |
| [CI/CD Integration](./docs/ci-cd.md) | GitHub Actions and pipeline mode | | [CI/CD Integration](./docs/ci-cd.md) | GitHub Actions and pipeline mode |
| [Changelog](./CHANGELOG.md) ([日本語](./docs/CHANGELOG.ja.md)) | Version history | | [Changelog](./CHANGELOG.md) ([日本語](./docs/CHANGELOG.ja.md)) | Version history |

View File

@ -15,8 +15,6 @@
- **4層ファセット解決**: 3層project → user → builtinから4層package-local → project → user → builtinに拡張 — ensemble パッケージのピースは自パッケージ内のファセットを最優先で解決 - **4層ファセット解決**: 3層project → user → builtinから4層package-local → project → user → builtinに拡張 — ensemble パッケージのピースは自パッケージ内のファセットを最優先で解決
- **ピース選択に ensemble カテゴリ追加**: インストール済みの ensemble パッケージがピース選択 UI の「ensemble」カテゴリにサブカテゴリとして自動表示 - **ピース選択に ensemble カテゴリ追加**: インストール済みの ensemble パッケージがピース選択 UI の「ensemble」カテゴリにサブカテゴリとして自動表示
- **implement/fix インストラクションにビルドゲート追加**: `implement``fix` のビルトインインストラクションでテスト実行前にビルド(型チェック)の実行を必須化 - **implement/fix インストラクションにビルドゲート追加**: `implement``fix` のビルトインインストラクションでテスト実行前にビルド(型チェック)の実行を必須化
- **TAKT Pack 仕様** (`docs/takt-pack-spec.md`): TAKT パッケージマニフェストのフォーマット仕様ドキュメント
### Changed ### Changed
- **BREAKING: ファセットディレクトリ構造の変更**: 全レイヤーでファセットディレクトリが `facets/` サブディレクトリ配下に移動 — `builtins/{lang}/{facetType}/``builtins/{lang}/facets/{facetType}/``~/.takt/{facetType}/``~/.takt/facets/{facetType}/``.takt/{facetType}/``.takt/facets/{facetType}/`。マイグレーション: カスタムファセットファイルを新しい `facets/` サブディレクトリに移動してください - **BREAKING: ファセットディレクトリ構造の変更**: 全レイヤーでファセットディレクトリが `facets/` サブディレクトリ配下に移動 — `builtins/{lang}/{facetType}/``builtins/{lang}/facets/{facetType}/``~/.takt/{facetType}/``~/.takt/facets/{facetType}/``.takt/{facetType}/``.takt/facets/{facetType}/`。マイグレーション: カスタムファセットファイルを新しい `facets/` サブディレクトリに移動してください

View File

@ -261,7 +261,6 @@ await engine.run();
| [Agent Guide](./agents.md) | カスタムエージェントの設定 | | [Agent Guide](./agents.md) | カスタムエージェントの設定 |
| [Builtin Catalog](./builtin-catalog.ja.md) | ビルトイン piece・persona の一覧 | | [Builtin Catalog](./builtin-catalog.ja.md) | ビルトイン piece・persona の一覧 |
| [Faceted Prompting](./faceted-prompting.ja.md) | プロンプト設計の方法論 | | [Faceted Prompting](./faceted-prompting.ja.md) | プロンプト設計の方法論 |
| [TAKT Pack Spec](./takt-pack-spec.md) | Ensemble パッケージのフォーマット仕様 |
| [Task Management](./task-management.ja.md) | タスクの追加・実行・隔離 | | [Task Management](./task-management.ja.md) | タスクの追加・実行・隔離 |
| [CI/CD Integration](./ci-cd.ja.md) | GitHub Actions・パイプラインモード | | [CI/CD Integration](./ci-cd.ja.md) | GitHub Actions・パイプラインモード |
| [Changelog](../CHANGELOG.md) ([日本語](./CHANGELOG.ja.md)) | バージョン履歴 | | [Changelog](../CHANGELOG.md) ([日本語](./CHANGELOG.ja.md)) | バージョン履歴 |

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync, readFileSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
import { join } from 'node:path';
import { parse as parseYaml } from 'yaml';
import { createIsolatedEnv, type IsolatedEnv } from '../helpers/isolated-env';
import { runTakt } from '../helpers/takt-runner';
type LockFile = {
source?: string;
ref?: string;
commit?: string;
imported_at?: string;
};
function canAccessRepo(repo: string): boolean {
try {
execFileSync('gh', ['repo', 'view', repo], { stdio: 'pipe' });
return true;
} catch {
return false;
}
}
function canAccessRepoRef(repo: string, ref: string): boolean {
try {
const out = execFileSync('gh', ['api', `/repos/${repo}/git/ref/tags/${ref}`], {
encoding: 'utf-8',
stdio: 'pipe',
});
return out.includes('"ref"');
} catch {
return false;
}
}
function readYamlFile<T>(path: string): T {
const raw = readFileSync(path, 'utf-8');
return parseYaml(raw) as T;
}
const FIXTURE_REPO = 'nrslib/takt-pack-fixture';
const FIXTURE_REPO_SUBDIR = 'nrslib/takt-pack-fixture-subdir';
const FIXTURE_REPO_FACETS_ONLY = 'nrslib/takt-pack-fixture-facets-only';
const MISSING_MANIFEST_REPO = 'nrslib/takt';
const FIXTURE_REF = 'v1.0.0';
const canUseFixtureRepo = canAccessRepo(FIXTURE_REPO) && canAccessRepoRef(FIXTURE_REPO, FIXTURE_REF);
const canUseSubdirRepo = canAccessRepo(FIXTURE_REPO_SUBDIR) && canAccessRepoRef(FIXTURE_REPO_SUBDIR, FIXTURE_REF);
const canUseFacetsOnlyRepo = canAccessRepo(FIXTURE_REPO_FACETS_ONLY) && canAccessRepoRef(FIXTURE_REPO_FACETS_ONLY, FIXTURE_REF);
const canUseMissingManifestRepo = canAccessRepo(MISSING_MANIFEST_REPO);
describe('E2E: takt ensemble (real GitHub fixtures)', () => {
let isolatedEnv: IsolatedEnv;
beforeEach(() => {
isolatedEnv = createIsolatedEnv();
});
afterEach(() => {
try {
isolatedEnv.cleanup();
} catch {
// best-effort
}
});
it.skipIf(!canUseFixtureRepo)('should install fixture package from GitHub and create lock file', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain(`📦 ${FIXTURE_REPO} @${FIXTURE_REF}`);
expect(result.stdout).toContain('インストールしました');
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
expect(existsSync(join(packageDir, 'takt-package.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-pack-lock.yaml'))).toBe(true);
expect(existsSync(join(packageDir, 'facets'))).toBe(true);
expect(existsSync(join(packageDir, 'pieces'))).toBe(true);
const lock = readYamlFile<LockFile>(join(packageDir, '.takt-pack-lock.yaml'));
expect(lock.source).toBe('github:nrslib/takt-pack-fixture');
expect(lock.ref).toBe(FIXTURE_REF);
expect(lock.commit).toBeTypeOf('string');
expect(lock.commit!.length).toBeGreaterThanOrEqual(7);
expect(lock.imported_at).toBeTypeOf('string');
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should list installed package after add', () => {
const addResult = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(addResult.exitCode).toBe(0);
const listResult = runTakt({
args: ['ensemble', 'list'],
cwd: process.cwd(),
env: isolatedEnv.env,
timeout: 120_000,
});
expect(listResult.exitCode).toBe(0);
expect(listResult.stdout).toContain('@nrslib/takt-pack-fixture');
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should remove installed package with confirmation', () => {
const addResult = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(addResult.exitCode).toBe(0);
const removeResult = runTakt({
args: ['ensemble', 'remove', '@nrslib/takt-pack-fixture'],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 120_000,
});
expect(removeResult.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should cancel installation when user answers N', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'n\n',
timeout: 240_000,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('キャンセルしました');
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseSubdirRepo)('should install subdir fixture package', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO_SUBDIR}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(result.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture-subdir');
expect(existsSync(join(packageDir, 'takt-package.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-pack-lock.yaml'))).toBe(true);
expect(existsSync(join(packageDir, 'facets')) || existsSync(join(packageDir, 'pieces'))).toBe(true);
}, 240_000);
it.skipIf(!canUseFacetsOnlyRepo)('should install facets-only fixture package without requiring pieces directory', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${FIXTURE_REPO_FACETS_ONLY}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(result.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-pack-fixture-facets-only');
expect(existsSync(join(packageDir, 'facets'))).toBe(true);
expect(existsSync(join(packageDir, 'pieces'))).toBe(false);
}, 240_000);
it.skipIf(!canUseMissingManifestRepo)('should fail when repository has no takt-package.yaml', () => {
const result = runTakt({
args: ['ensemble', 'add', `github:${MISSING_MANIFEST_REPO}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(result.exitCode).not.toBe(0);
expect(result.stdout).toContain('takt-package.yaml not found');
}, 240_000);
});

View File

@ -0,0 +1,166 @@
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { mkdirSync, writeFileSync } from 'node:fs';
import { join, resolve, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { createIsolatedEnv, updateIsolatedConfig, type IsolatedEnv } from '../helpers/isolated-env';
import { createTestRepo, type TestRepo } from '../helpers/test-repo';
import { runTakt } from '../helpers/takt-runner';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
function writeAgent(baseDir: string): void {
const agentsDir = join(baseDir, 'agents');
mkdirSync(agentsDir, { recursive: true });
writeFileSync(
join(agentsDir, 'test-coder.md'),
'You are a test coder. Complete the task exactly and respond with Done.',
'utf-8',
);
}
function writeMinimalPiece(piecePath: string): void {
const pieceDir = dirname(piecePath);
mkdirSync(pieceDir, { recursive: true });
writeFileSync(
piecePath,
[
'name: e2e-branch-piece',
'description: Piece for branch coverage E2E',
'max_movements: 3',
'movements:',
' - name: execute',
' edit: true',
' persona: ../agents/test-coder.md',
' allowed_tools:',
' - Read',
' - Write',
' - Edit',
' required_permission_mode: edit',
' instruction_template: |',
' {task}',
' rules:',
' - condition: Done',
' next: COMPLETE',
'',
].join('\n'),
'utf-8',
);
}
function runTaskWithPiece(args: {
piece?: string;
cwd: string;
env: NodeJS.ProcessEnv;
}): ReturnType<typeof runTakt> {
const scenarioPath = resolve(__dirname, '../fixtures/scenarios/execute-done.json');
const baseArgs = ['--task', 'Create a file called noop.txt', '--create-worktree', 'no', '--provider', 'mock'];
const fullArgs = args.piece ? [...baseArgs, '--piece', args.piece] : baseArgs;
return runTakt({
args: fullArgs,
cwd: args.cwd,
env: {
...args.env,
TAKT_MOCK_SCENARIO: scenarioPath,
},
timeout: 240_000,
});
}
describe('E2E: Piece selection branch coverage', () => {
let isolatedEnv: IsolatedEnv;
let testRepo: TestRepo;
beforeEach(() => {
isolatedEnv = createIsolatedEnv();
testRepo = createTestRepo();
updateIsolatedConfig(isolatedEnv.taktDir, {
provider: 'mock',
model: 'mock-model',
enable_builtin_pieces: false,
});
});
afterEach(() => {
try {
testRepo.cleanup();
} catch {
// best-effort
}
try {
isolatedEnv.cleanup();
} catch {
// best-effort
}
});
it('should execute when --piece is a file path (isPiecePath branch)', () => {
const customPiecePath = join(testRepo.path, '.takt', 'pieces', 'path-piece.yaml');
writeAgent(join(testRepo.path, '.takt'));
writeMinimalPiece(customPiecePath);
const result = runTaskWithPiece({
piece: customPiecePath,
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece completed');
}, 240_000);
it('should execute when --piece is a known local name (resolver hit branch)', () => {
writeAgent(join(testRepo.path, '.takt'));
writeMinimalPiece(join(testRepo.path, '.takt', 'pieces', 'local-piece.yaml'));
const result = runTaskWithPiece({
piece: 'local-piece',
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece completed');
}, 240_000);
it('should execute when --piece is an ensemble @scope name (resolver hit branch)', () => {
const pkgRoot = join(isolatedEnv.taktDir, 'ensemble', '@nrslib', 'takt-packages');
writeAgent(pkgRoot);
writeMinimalPiece(join(pkgRoot, 'pieces', 'critical-thinking.yaml'));
const result = runTaskWithPiece({
piece: '@nrslib/takt-packages/critical-thinking',
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece completed');
expect(result.stdout).not.toContain('Piece not found');
}, 240_000);
it('should fail fast with message when --piece is unknown (resolver miss branch)', () => {
const result = runTaskWithPiece({
piece: '@nrslib/takt-packages/not-found',
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece not found: @nrslib/takt-packages/not-found');
expect(result.stdout).toContain('Cancelled');
}, 240_000);
it('should execute when --piece is omitted (selectPiece branch)', () => {
writeAgent(join(testRepo.path, '.takt'));
writeMinimalPiece(join(testRepo.path, '.takt', 'pieces', 'default.yaml'));
const result = runTaskWithPiece({
cwd: testRepo.path,
env: isolatedEnv.env,
});
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('Piece completed');
}, 240_000);
});