feat: add quant engine WBS verification harness
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
test.describe('QE-M1-02: Collection Run List & Detail Verification', () => {
|
||||
// Login before each test
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/Account/Login');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Fill login form with credentials (admin/admin)
|
||||
const usernameInput = page.locator('#username');
|
||||
const passwordInput = page.locator('#password');
|
||||
const loginButton = page.locator('#loginBtn');
|
||||
|
||||
await usernameInput.fill('admin');
|
||||
await passwordInput.fill('admin');
|
||||
await loginButton.click();
|
||||
|
||||
// Wait for login to complete
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test('QE-M1-02: Collection run renders in list with API-derived expected values', async ({ page }) => {
|
||||
// Step 1: Fetch expected values from API (source of truth)
|
||||
const apiResponse = await page.request.get('/api/collection/runs?limit=20');
|
||||
expect(apiResponse.ok()).toBeTruthy();
|
||||
|
||||
const responseJson = await apiResponse.json();
|
||||
const runs = (responseJson as any).runs || [];
|
||||
|
||||
// Fail if no collection runs exist in database
|
||||
if (runs.length === 0) {
|
||||
throw new Error(
|
||||
'No collection runs in DB — run the daily-collection job first. ' +
|
||||
'Expected at least 1 run from kis_collection_runs table.'
|
||||
);
|
||||
}
|
||||
|
||||
// Extract expected values from most recent run (first in list)
|
||||
const expectedRun = runs[0];
|
||||
const expectedRunId = expectedRun.runId;
|
||||
const expectedTotalSnapshots = expectedRun.totalSnapshots ?? 0;
|
||||
const expectedStatus = expectedRun.status; // e.g., "completed", "running", "failed"
|
||||
|
||||
// Map status to Korean text (same logic as Index.cshtml — unknown statuses
|
||||
// like COMPLETED_WITH_ERRORS render the raw status string in a secondary badge)
|
||||
let expectedStatusText = String(expectedStatus ?? '');
|
||||
if (expectedStatus?.toLowerCase() === 'completed') {
|
||||
expectedStatusText = '완료';
|
||||
} else if (expectedStatus?.toLowerCase() === 'running') {
|
||||
expectedStatusText = '진행 중';
|
||||
} else if (expectedStatus?.toLowerCase() === 'failed') {
|
||||
expectedStatusText = '실패';
|
||||
}
|
||||
|
||||
console.log(
|
||||
`\n=== QE-M1-02 Test Started ===\n` +
|
||||
`Expected RunId: ${expectedRunId}\n` +
|
||||
`Expected TotalSnapshots: ${expectedTotalSnapshots}\n` +
|
||||
`Expected Status: ${expectedStatus} (rendered as: ${expectedStatusText})\n`
|
||||
);
|
||||
|
||||
// Step 2: Navigate to Collection admin page
|
||||
await page.goto('/Admin/Collection');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Step 3: Verify page title contains "데이터 수집" (collection)
|
||||
const pageTitle = await page.title();
|
||||
expect(pageTitle).toContain('데이터 수집');
|
||||
|
||||
// Step 4: Assert that a row containing the expected runId is visible
|
||||
const runIdCell = page.locator(`td:has-text("${expectedRunId}")`);
|
||||
await expect(runIdCell).toBeVisible();
|
||||
console.log(`✓ RunId row found and visible: ${expectedRunId}`);
|
||||
|
||||
// Step 5: Find the row containing this runId and verify the snapshot count
|
||||
const tableRow = runIdCell.locator('xpath=ancestor::tr');
|
||||
|
||||
// Within the row, find all td elements and map to columns
|
||||
// Columns: 실행 ID (0), 시작 시간 (1), 종료 시간 (2), 상태 (3), 스냅샷 수 (4), 오류 수 (5)
|
||||
const cells = tableRow.locator('td');
|
||||
const cellCount = await cells.count();
|
||||
expect(cellCount).toBeGreaterThanOrEqual(5); // At least 5 columns
|
||||
|
||||
// Cell 4 (index 4) is "스냅샷 수" (total snapshots)
|
||||
const snapshotCell = cells.nth(4);
|
||||
const snapshotText = await snapshotCell.textContent();
|
||||
expect(snapshotText?.trim()).toBe(String(expectedTotalSnapshots));
|
||||
console.log(`✓ Snapshot count matches: ${snapshotText?.trim()} == ${expectedTotalSnapshots}`);
|
||||
|
||||
// Cell 3 (index 3) is "상태" (status badge)
|
||||
const statusCell = cells.nth(3);
|
||||
const statusBadge = statusCell.locator('span.badge');
|
||||
const statusBadgeText = await statusBadge.textContent();
|
||||
expect(statusBadgeText?.trim()).toBe(expectedStatusText);
|
||||
console.log(`✓ Status badge matches: ${statusBadgeText?.trim()} == ${expectedStatusText}`);
|
||||
|
||||
// Step 6: Create screenshot directory and take screenshot of collection list
|
||||
const screenshotDir = path.join(process.cwd(), 'Temp', 'evidence', 'QE-M1-02', 'screenshots');
|
||||
fs.mkdirSync(screenshotDir, { recursive: true });
|
||||
|
||||
await page.screenshot({
|
||||
path: path.join(screenshotDir, '01-collection-page.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(`✓ Screenshot saved: 01-collection-page.png`);
|
||||
|
||||
// Step 7: Navigate to the run detail page
|
||||
// The detail page route is /Admin/Collection/{runId}
|
||||
await page.goto(`/Admin/Collection/${expectedRunId}`);
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Step 8: Verify detail page title contains the runId
|
||||
const detailPageTitle = await page.title();
|
||||
expect(detailPageTitle).toContain('수집 실행 상세');
|
||||
|
||||
// Step 9: Verify that the RunId is displayed on the detail page
|
||||
// The page title shows: "수집 실행 상세 - {runId}"
|
||||
const pageHeading = page.locator('h2.page-title');
|
||||
const headingText = await pageHeading.textContent();
|
||||
expect(headingText).toContain(expectedRunId);
|
||||
console.log(`✓ Detail page title contains RunId: ${headingText}`);
|
||||
|
||||
// Step 10: Verify snapshots count is displayed on detail page
|
||||
// The snapshot count appears in a card with "스냅샷 수" as the title
|
||||
const snapshotCountCard = page.locator('h4.card-title:has-text("스냅샷 수")');
|
||||
await expect(snapshotCountCard).toBeVisible();
|
||||
|
||||
// The value is in a div with class h6 after the title
|
||||
const snapshotCountValue = snapshotCountCard.locator('xpath=following-sibling::div[1]');
|
||||
const countText = await snapshotCountValue.textContent();
|
||||
expect(countText?.trim()).toBe(String(expectedTotalSnapshots));
|
||||
console.log(`✓ Detail page snapshot count matches: ${countText?.trim()} == ${expectedTotalSnapshots}`);
|
||||
|
||||
// Step 11: Take screenshot of detail page
|
||||
await page.screenshot({
|
||||
path: path.join(screenshotDir, '02-run-detail.png'),
|
||||
fullPage: true,
|
||||
});
|
||||
console.log(`✓ Screenshot saved: 02-run-detail.png`);
|
||||
|
||||
console.log(
|
||||
`\n=== QE-M1-02 Test Completed Successfully ===\n` +
|
||||
`Evidence files saved to: ${screenshotDir}\n`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,498 @@
|
||||
"""
|
||||
Tests for verify_wbs_task_v1.py and validate_quant_engine_wbs_v1.py
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
# Import the modules to test
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
||||
from tools.verify_wbs_task_v1 import parse_dotnet_connection_string
|
||||
|
||||
|
||||
class TestParseDotnetConnectionString:
|
||||
"""Test .NET connection string parsing."""
|
||||
|
||||
def test_parse_basic_connection_string(self):
|
||||
"""Test parsing a basic connection string."""
|
||||
s = "Host=localhost;Port=5432;Database=testdb;Username=user;Password=pass;"
|
||||
result = parse_dotnet_connection_string(s)
|
||||
assert result["host"] == "localhost"
|
||||
assert result["port"] == "5432"
|
||||
assert result["dbname"] == "testdb"
|
||||
assert result["user"] == "user"
|
||||
assert result["password"] == "pass"
|
||||
assert "options" not in result
|
||||
|
||||
def test_parse_with_search_path(self):
|
||||
"""Test parsing with Search Path."""
|
||||
s = "Host=localhost;Database=testdb;Username=user;Password=pass;Search Path=myschema;"
|
||||
result = parse_dotnet_connection_string(s)
|
||||
assert result["host"] == "localhost"
|
||||
assert result["dbname"] == "testdb"
|
||||
assert result["options"] == "-c search_path=myschema"
|
||||
|
||||
def test_parse_case_insensitive(self):
|
||||
"""Test case-insensitive key handling."""
|
||||
s = "HOST=localhost;DATABASE=testdb;USERNAME=user;PASSWORD=pass;"
|
||||
result = parse_dotnet_connection_string(s)
|
||||
assert result["host"] == "localhost"
|
||||
assert result["dbname"] == "testdb"
|
||||
assert result["user"] == "user"
|
||||
assert result["password"] == "pass"
|
||||
|
||||
def test_parse_unknown_keys_ignored(self):
|
||||
"""Test that unknown keys are ignored."""
|
||||
s = "Host=localhost;UnknownKey=value;Database=testdb;"
|
||||
result = parse_dotnet_connection_string(s)
|
||||
assert result["host"] == "localhost"
|
||||
assert result["dbname"] == "testdb"
|
||||
assert "UnknownKey" not in result and "unknownkey" not in result
|
||||
|
||||
def test_parse_empty_string(self):
|
||||
"""Test parsing empty string."""
|
||||
result = parse_dotnet_connection_string("")
|
||||
assert result == {}
|
||||
|
||||
def test_parse_with_spaces(self):
|
||||
"""Test parsing with extra spaces."""
|
||||
s = " Host = localhost ; Database = testdb ; "
|
||||
result = parse_dotnet_connection_string(s)
|
||||
assert result["host"] == "localhost"
|
||||
assert result["dbname"] == "testdb"
|
||||
|
||||
|
||||
class TestJsonGateComparison:
|
||||
"""Test json_gate comparison operator parsing."""
|
||||
|
||||
def test_json_gate_exact_equality(self, tmp_path):
|
||||
"""Test exact equality in json_gate."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-01": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "json_gate",
|
||||
"path": "Temp/test_output.json",
|
||||
"expect": {"gate": "PASS", "count": 5}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create test artifact
|
||||
artifact_path = repo_root / "Temp" / "test_output.json"
|
||||
artifact_path.parent.mkdir(parents=True)
|
||||
artifact_path.write_text(json.dumps({"gate": "PASS", "count": 5}), encoding="utf-8")
|
||||
|
||||
# Run verify_wbs_task_v1
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-01",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
def test_json_gate_greater_equal_comparison(self, tmp_path):
|
||||
"""Test >= comparison in json_gate."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-02": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "json_gate",
|
||||
"path": "Temp/test_output.json",
|
||||
"expect": {"compared_count": ">=20"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create test artifact - PASS case
|
||||
artifact_path = repo_root / "Temp" / "test_output.json"
|
||||
artifact_path.parent.mkdir(parents=True)
|
||||
artifact_path.write_text(json.dumps({"compared_count": 25}), encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-02",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
def test_json_gate_greater_equal_fail(self, tmp_path):
|
||||
"""Test >= comparison fails when below threshold."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-03": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "json_gate",
|
||||
"path": "Temp/test_output.json",
|
||||
"expect": {"compared_count": ">=20"}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create test artifact - FAIL case (value too low)
|
||||
artifact_path = repo_root / "Temp" / "test_output.json"
|
||||
artifact_path.parent.mkdir(parents=True)
|
||||
artifact_path.write_text(json.dumps({"compared_count": 15}), encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-03",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 1, f"Expected failure, got: {result.stdout}"
|
||||
|
||||
|
||||
class TestLogPatternCheck:
|
||||
"""Test log_pattern check type."""
|
||||
|
||||
def test_log_pattern_min_matches_pass(self, tmp_path):
|
||||
"""Test log_pattern with min_matches that passes."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-04": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "log_pattern",
|
||||
"file_glob": "Temp/test.log",
|
||||
"pattern": "SUCCESS",
|
||||
"expect": {"min_matches": 1}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create log file with matches
|
||||
log_path = repo_root / "Temp" / "test.log"
|
||||
log_path.parent.mkdir(parents=True)
|
||||
log_path.write_text("Operation SUCCESS\nAnother line\nOperation SUCCESS\n", encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-04",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
def test_log_pattern_max_matches_zero(self, tmp_path):
|
||||
"""Test log_pattern with max_matches=0 (no file should match)."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-05": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "log_pattern",
|
||||
"file_glob": "Temp/nonexistent.log",
|
||||
"pattern": "ERROR",
|
||||
"expect": {"max_matches": 0}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-05",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
# Should pass because file doesn't exist = 0 matches
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
class TestFileExistsCheck:
|
||||
"""Test file_exists check type."""
|
||||
|
||||
def test_file_exists_with_min_bytes_fail(self, tmp_path):
|
||||
"""Test file_exists fails when file is too small."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-06": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{
|
||||
"type": "file_exists",
|
||||
"paths": ["Temp/small_file.txt"],
|
||||
"expect": {"min_bytes": 100}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create a file smaller than min_bytes
|
||||
file_path = repo_root / "Temp" / "small_file.txt"
|
||||
file_path.parent.mkdir(parents=True)
|
||||
file_path.write_text("small", encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "verify_wbs_task_v1.py"), "--task", "TEST-06",
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 1, f"Expected failure, got: {result.stdout}"
|
||||
|
||||
|
||||
class TestValidatorLogic:
|
||||
"""Test validate_quant_engine_wbs_v1.py logic."""
|
||||
|
||||
def test_validator_done_task_with_passing_verdict(self, tmp_path):
|
||||
"""Test validator passes when DONE task has PASS verdict."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-07": {
|
||||
"title": "Test task",
|
||||
"status": "DONE",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": ["Temp/evidence/TEST-07/verdict.json"],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [{"type": "file_exists", "paths": ["spec/60_quant_engine_wbs.yaml"]}]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create a passing verdict
|
||||
verdict_path = repo_root / "Temp" / "evidence" / "TEST-07" / "verdict.json"
|
||||
verdict_path.parent.mkdir(parents=True)
|
||||
verdict_path.write_text(
|
||||
json.dumps({
|
||||
"task_id": "TEST-07",
|
||||
"formula_id": "QUANT_ENGINE_WBS_TASK_V1",
|
||||
"gate": "PASS",
|
||||
"checks": []
|
||||
}),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
# Create roadmap pointer
|
||||
roadmap_path = repo_root / "docs" / "ROADMAP_WBS.md"
|
||||
roadmap_path.parent.mkdir(parents=True)
|
||||
roadmap_path.write_text("# Roadmap\n\nQUANT_ENGINE_WBS_V1 is the standard.\n", encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "validate_quant_engine_wbs_v1.py"),
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
def test_validator_done_task_without_verdict(self, tmp_path):
|
||||
"""Test validator fails when DONE task is missing verdict."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-08": {
|
||||
"title": "Test task",
|
||||
"status": "DONE",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": []
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create roadmap pointer
|
||||
roadmap_path = repo_root / "docs" / "ROADMAP_WBS.md"
|
||||
roadmap_path.parent.mkdir(parents=True)
|
||||
roadmap_path.write_text("# Roadmap\n\nQUANT_ENGINE_WBS_V1 is the standard.\n", encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "validate_quant_engine_wbs_v1.py"),
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 1, f"Expected failure, got: {result.stdout}"
|
||||
|
||||
def test_validator_pending_tasks_no_verdict_required(self, tmp_path):
|
||||
"""Test validator passes for PENDING tasks without verdicts."""
|
||||
repo_root = tmp_path / "repo"
|
||||
repo_root.mkdir()
|
||||
spec_path = repo_root / "spec" / "60_quant_engine_wbs.yaml"
|
||||
spec_path.parent.mkdir()
|
||||
|
||||
spec = {
|
||||
"meta": {"formula_id": "QUANT_ENGINE_WBS_V1"},
|
||||
"tasks": {
|
||||
"TEST-09": {
|
||||
"title": "Test task",
|
||||
"status": "PENDING",
|
||||
"depends_on": [],
|
||||
"success_criteria": {
|
||||
"expected_success_value": {},
|
||||
"evidence_artifacts": [],
|
||||
"verification_commands": []
|
||||
},
|
||||
"evidence_checks": [
|
||||
{"type": "file_exists", "paths": ["spec/60_quant_engine_wbs.yaml"]}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
spec_path.write_text(yaml.dump(spec), encoding="utf-8")
|
||||
|
||||
# Create roadmap pointer
|
||||
roadmap_path = repo_root / "docs" / "ROADMAP_WBS.md"
|
||||
roadmap_path.parent.mkdir(parents=True)
|
||||
roadmap_path.write_text("# Roadmap\n\nQUANT_ENGINE_WBS_V1 is the standard.\n", encoding="utf-8")
|
||||
|
||||
tools_dir = Path(__file__).resolve().parents[2] / "tools"
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(tools_dir / "validate_quant_engine_wbs_v1.py"),
|
||||
"--repo-root", str(repo_root), "--spec", str(spec_path)],
|
||||
cwd=repo_root,
|
||||
capture_output=True,
|
||||
text=True
|
||||
)
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user