takt/e2e/specs/repertoire-real.e2e.ts
nrs 4ee69f857a
add-e2e-coverage (#364)
* takt: add-e2e-coverage

* takt: add-e2e-coverage
2026-02-23 13:00:48 +09:00

430 lines
14 KiB
TypeScript

// E2E更新時は docs/testing/e2e.md も更新すること
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { existsSync, mkdirSync, 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 fixtureHasManifest(repo: string, ref: string, filename: string): boolean {
try {
const out = execFileSync(
'gh',
['api', `/repos/${repo}/git/trees/${ref}`, '--recursive'],
{ encoding: 'utf-8', stdio: 'pipe' },
);
const tree = JSON.parse(out) as { tree: { path: string }[] };
return tree.tree.some((f) => f.path === filename);
} 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-repertoire-fixture';
const FIXTURE_REPO_SUBDIR = 'nrslib/takt-repertoire-fixture-subdir';
const FIXTURE_REPO_FACETS_ONLY = 'nrslib/takt-repertoire-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) &&
fixtureHasManifest(FIXTURE_REPO, FIXTURE_REF, 'takt-repertoire.yaml');
const canUseSubdirRepo =
canAccessRepo(FIXTURE_REPO_SUBDIR) &&
canAccessRepoRef(FIXTURE_REPO_SUBDIR, FIXTURE_REF) &&
fixtureHasManifest(FIXTURE_REPO_SUBDIR, FIXTURE_REF, 'takt-repertoire.yaml');
const canUseFacetsOnlyRepo =
canAccessRepo(FIXTURE_REPO_FACETS_ONLY) &&
canAccessRepoRef(FIXTURE_REPO_FACETS_ONLY, FIXTURE_REF) &&
fixtureHasManifest(FIXTURE_REPO_FACETS_ONLY, FIXTURE_REF, 'takt-repertoire.yaml');
const canUseMissingManifestRepo = canAccessRepo(MISSING_MANIFEST_REPO);
describe('E2E: takt repertoire (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: ['repertoire', '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, 'repertoire', '@nrslib', 'takt-repertoire-fixture');
expect(existsSync(join(packageDir, 'takt-repertoire.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-repertoire-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-repertoire-lock.yaml'));
expect(lock.source).toBe('github:nrslib/takt-repertoire-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: ['repertoire', '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: ['repertoire', 'list'],
cwd: process.cwd(),
env: isolatedEnv.env,
timeout: 120_000,
});
expect(listResult.exitCode).toBe(0);
expect(listResult.stdout).toContain('@nrslib/takt-repertoire-fixture');
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should remove installed package with confirmation', () => {
const addResult = runTakt({
args: ['repertoire', '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: ['repertoire', 'remove', '@nrslib/takt-repertoire-fixture'],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 120_000,
});
expect(removeResult.exitCode).toBe(0);
const packageDir = join(isolatedEnv.taktDir, 'repertoire', '@nrslib', 'takt-repertoire-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseFixtureRepo)('should cancel installation when user answers N', () => {
const result = runTakt({
args: ['repertoire', '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, 'repertoire', '@nrslib', 'takt-repertoire-fixture');
expect(existsSync(packageDir)).toBe(false);
}, 240_000);
it.skipIf(!canUseSubdirRepo)('should install subdir fixture package', () => {
const result = runTakt({
args: ['repertoire', '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, 'repertoire', '@nrslib', 'takt-repertoire-fixture-subdir');
expect(existsSync(join(packageDir, 'takt-repertoire.yaml'))).toBe(true);
expect(existsSync(join(packageDir, '.takt-repertoire-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: ['repertoire', '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, 'repertoire', '@nrslib', 'takt-repertoire-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-repertoire.yaml', () => {
const result = runTakt({
args: ['repertoire', '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-repertoire.yaml not found');
}, 240_000);
it.skipIf(!canUseFixtureRepo)(
'should display pre-install summary with package name, faceted info, and pieces list',
() => {
const result = runTakt({
args: ['repertoire', '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(`nrslib/takt-repertoire-fixture @${FIXTURE_REF}`);
expect(result.stdout).toContain('facets:');
expect(result.stdout).toContain('pieces:');
expect(result.stdout).toContain('キャンセルしました');
},
240_000,
);
it.skipIf(!canUseFixtureRepo)(
'should display already-installed warning when adding a package that is already installed',
() => {
const firstResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(firstResult.exitCode).toBe(0);
const secondResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\nn\n',
timeout: 240_000,
});
expect(secondResult.exitCode).toBe(0);
expect(secondResult.stdout).toContain('既にインストールされています');
},
240_000,
);
it.skipIf(!canUseFixtureRepo)(
'should atomically update package with no leftover tmp or bak directories when user answers y to overwrite',
() => {
const firstResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(firstResult.exitCode).toBe(0);
const packageDir = join(
isolatedEnv.taktDir,
'repertoire',
'@nrslib',
'takt-repertoire-fixture',
);
const secondResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\ny\n',
timeout: 240_000,
});
expect(secondResult.exitCode).toBe(0);
expect(existsSync(`${packageDir}.tmp`)).toBe(false);
expect(existsSync(`${packageDir}.bak`)).toBe(false);
expect(existsSync(join(packageDir, '.takt-repertoire-lock.yaml'))).toBe(true);
},
240_000,
);
it.skipIf(!canUseFixtureRepo)(
'should keep existing package unchanged when user answers N to overwrite prompt',
() => {
const firstResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(firstResult.exitCode).toBe(0);
const lockPath = join(
isolatedEnv.taktDir,
'repertoire',
'@nrslib',
'takt-repertoire-fixture',
'.takt-repertoire-lock.yaml',
);
const originalLock = readYamlFile<LockFile>(lockPath);
const secondResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\nn\n',
timeout: 240_000,
});
expect(secondResult.exitCode).toBe(0);
const afterLock = readYamlFile<LockFile>(lockPath);
expect(afterLock.commit).toBe(originalLock.commit);
expect(afterLock.imported_at).toBe(originalLock.imported_at);
},
240_000,
);
it.skipIf(!canUseFixtureRepo)(
'should clean up leftover .tmp/ directory from previous failed installation and succeed',
() => {
const firstResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(firstResult.exitCode).toBe(0);
const packageDir = join(
isolatedEnv.taktDir,
'repertoire',
'@nrslib',
'takt-repertoire-fixture',
);
mkdirSync(`${packageDir}.tmp`, { recursive: true });
const secondResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\ny\n',
timeout: 240_000,
});
expect(secondResult.exitCode).toBe(0);
expect(existsSync(`${packageDir}.tmp`)).toBe(false);
},
240_000,
);
it.skipIf(!canUseFixtureRepo)(
'should clean up leftover .bak/ directory from previous failed installation and succeed',
() => {
const firstResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\n',
timeout: 240_000,
});
expect(firstResult.exitCode).toBe(0);
const packageDir = join(
isolatedEnv.taktDir,
'repertoire',
'@nrslib',
'takt-repertoire-fixture',
);
mkdirSync(`${packageDir}.bak`, { recursive: true });
const secondResult = runTakt({
args: ['repertoire', 'add', `github:${FIXTURE_REPO}@${FIXTURE_REF}`],
cwd: process.cwd(),
env: isolatedEnv.env,
input: 'y\ny\n',
timeout: 240_000,
});
expect(secondResult.exitCode).toBe(0);
expect(existsSync(`${packageDir}.bak`)).toBe(false);
},
240_000,
);
it.todo('should populate lock file commit field with the specified commit SHA when installing by SHA');
it.todo('should display warning symbol when package contains piece with edit: true');
it.todo('should reject takt-repertoire.yaml with absolute path in path field (/foo)');
it.todo('should reject takt-repertoire.yaml with path traversal via ".." segments');
it.todo('should reject package with neither facets/ nor pieces/ directory');
it.todo('should reject takt-repertoire.yaml with min_version "1.0" (missing patch segment)');
it.todo('should reject takt-repertoire.yaml with min_version "v1.0.0" (v prefix)');
it.todo('should reject takt-repertoire.yaml with min_version "1.0.0-alpha" (pre-release suffix)');
it.todo('should fail with version mismatch message when min_version exceeds current takt version');
});