feat: expand and secure mentor library
This commit is contained in:
+102
-1
@@ -47,6 +47,65 @@ function session(role = "admin", subscribed = true) {
|
||||
};
|
||||
}
|
||||
|
||||
function mentorDirectory(role = "admin") {
|
||||
const mentors = [
|
||||
{
|
||||
id: "source-a",
|
||||
name: "原帖老师",
|
||||
description: "依据长期实盘原帖提炼",
|
||||
tagline: "先看周期,再看机会。",
|
||||
focus: ["情绪周期", "仓位纪律"],
|
||||
evidence: { grade: "A", label: "实盘原帖", note: "长期原始实盘记录" },
|
||||
quality: { score: 6, total: 6, status: "pass" },
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: "source-b",
|
||||
name: "多源老师",
|
||||
description: "依据公开访谈与多源资料整理",
|
||||
tagline: "确认之后再行动。",
|
||||
focus: ["主线确认", "风险管理"],
|
||||
evidence: { grade: "B", label: "多源整理", note: "公开访谈与多源材料" },
|
||||
quality: { score: 6, total: 6, status: "pass" },
|
||||
private: false,
|
||||
},
|
||||
{
|
||||
id: "source-c",
|
||||
name: "推演老师",
|
||||
description: "公开语录较少,以行为推演为主",
|
||||
tagline: "只讨论可验证的行为。",
|
||||
focus: ["行为推演", "诚实边界"],
|
||||
evidence: { grade: "C", label: "行为推演", note: "公开语录较少" },
|
||||
quality: { score: 5, total: 6, status: "conditional" },
|
||||
private: false,
|
||||
},
|
||||
];
|
||||
for (let index = 1; index <= 18; index += 1) {
|
||||
const grade = ["A", "B", "C"][(index - 1) % 3];
|
||||
mentors.push({
|
||||
id: `extra-${index}`,
|
||||
name: `扩展模型${String(index).padStart(2, "0")}`,
|
||||
description: `用于验证完整目录密度的${grade}级思维模型`,
|
||||
tagline: "保持证据边界。",
|
||||
focus: ["市场结构", "条件预案"],
|
||||
evidence: { grade, label: { A: "原始语料", B: "多源整理", C: "行为材料" }[grade], note: `${grade}级测试素材` },
|
||||
quality: { score: 6, total: 6, status: "pass" },
|
||||
private: false,
|
||||
});
|
||||
}
|
||||
if (role === "admin") mentors.unshift({
|
||||
id: "private-owner",
|
||||
name: "私有老师",
|
||||
description: "依据个人复盘记录提炼",
|
||||
tagline: "只对自己开放。",
|
||||
focus: ["个人复盘", "交易纪律"],
|
||||
evidence: { grade: "A", label: "私有原始语料", note: "仅限管理员本人使用" },
|
||||
quality: { score: null, total: null, status: "private" },
|
||||
private: true,
|
||||
});
|
||||
return mentors;
|
||||
}
|
||||
|
||||
async function mockApplication(page, authSession = session()) {
|
||||
await page.route("**/api/**", async (route) => {
|
||||
const url = new URL(route.request().url());
|
||||
@@ -157,7 +216,9 @@ async function mockApplication(page, authSession = session()) {
|
||||
created_at: "2026-07-23T09:12:00+08:00",
|
||||
}],
|
||||
};
|
||||
} else if (url.pathname === "/api/mentors/setup") payload = { trade_date: "20260722", mentors: [] };
|
||||
} else if (url.pathname === "/api/mentors/setup") {
|
||||
payload = { trade_date: "20260722", mentors: mentorDirectory(authSession.user.role) };
|
||||
}
|
||||
else if (url.pathname === "/api/heaven/setup") {
|
||||
await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: "测试环境不加载问天数据" }) });
|
||||
return;
|
||||
@@ -430,3 +491,43 @@ test("heaven workspace controls fit a narrow viewport", async ({ page }) => {
|
||||
expect(heartControls.x + heartControls.width).toBeLessThanOrEqual(375);
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
test("mentor directory exposes evidence filters and private owner metadata", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 1440, height: 900 });
|
||||
await mockApplication(page, session("admin", true));
|
||||
await page.goto("/index.html");
|
||||
await page.locator('[data-view="mentorView"]').first().click();
|
||||
|
||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(22);
|
||||
await expect(page.locator('#mentorList [data-mentor-id="private-owner"] .mentor-badge.private')).toContainText("仅自己");
|
||||
await expect(page.locator('#mentorList [data-mentor-id="source-a"] .mentor-badge.grade-a')).toHaveText("A");
|
||||
await expect(page.locator('#mentorList [data-mentor-id="source-c"] .mentor-badge.quality')).toHaveText("5/6");
|
||||
|
||||
await page.locator("#mentorSearchInput").fill("行为推演");
|
||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(1);
|
||||
await expect(page.locator("#mentorCount")).toHaveText("1 / 22 位");
|
||||
await page.locator("#mentorSearchInput").fill("");
|
||||
await page.locator('[data-mentor-grade="B"]').click();
|
||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(7);
|
||||
await page.locator('#mentorList [data-mentor-id="source-b"]').click();
|
||||
await expect(page.locator("#activeMentorName")).toHaveText("多源老师");
|
||||
await expect(page.locator("#activeMentorBadges")).toContainText("B · 多源整理");
|
||||
await expect(page.locator("#activeMentorEvidence")).toHaveText("公开访谈与多源材料");
|
||||
});
|
||||
|
||||
test("mobile mentor directory opens as a searchable selector and hides private mentors", async ({ page }) => {
|
||||
await page.setViewportSize({ width: 375, height: 812 });
|
||||
await mockApplication(page, session("user", true));
|
||||
await page.goto("/index.html");
|
||||
await page.locator('[data-view="mentorView"]').first().click();
|
||||
|
||||
await expect(page.locator("#mentorDirectoryToggle")).toBeVisible();
|
||||
await page.locator("#mentorDirectoryToggle").click();
|
||||
await expect(page.locator("#mentorView .mentor-sidebar")).toHaveClass(/is-open/);
|
||||
await expect(page.locator("#mentorList .mentor-option")).toHaveCount(21);
|
||||
await expect(page.locator('#mentorList [data-mentor-id="private-owner"]')).toHaveCount(0);
|
||||
await page.locator('#mentorList [data-mentor-id="source-c"]').click();
|
||||
await expect(page.locator("#mentorView .mentor-sidebar")).not.toHaveClass(/is-open/);
|
||||
await expect(page.locator("#mobileActiveMentorName")).toHaveText("推演老师");
|
||||
expect(await page.evaluate(() => document.documentElement.scrollWidth - window.innerWidth)).toBeLessThanOrEqual(1);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ class DeploymentContractTests(unittest.TestCase):
|
||||
cls.compose = (ROOT / "compose.yaml").read_text(encoding="utf-8")
|
||||
cls.dockerfile = (ROOT / "Dockerfile").read_text(encoding="utf-8")
|
||||
cls.dockerignore = (ROOT / ".dockerignore").read_text(encoding="utf-8")
|
||||
cls.gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8")
|
||||
|
||||
def test_compose_exposes_only_requested_lan_port(self):
|
||||
self.assertIn('"0.0.0.0:8765:8765/tcp"', self.compose)
|
||||
@@ -26,8 +27,9 @@ class DeploymentContractTests(unittest.TestCase):
|
||||
self.assertIn('"--host", "0.0.0.0", "--port", "8765"', self.dockerfile)
|
||||
|
||||
def test_secrets_and_runtime_data_are_not_copied_into_image(self):
|
||||
for pattern in (".env", "data/*.db", "data/*.db-wal", "data/*.db-shm"):
|
||||
for pattern in (".env", "data/private-mentor-skills/", "data/*.db", "data/*.db-wal", "data/*.db-shm"):
|
||||
self.assertIn(pattern, self.dockerignore)
|
||||
self.assertIn("data/private-mentor-skills/", self.gitignore)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
from mentor_agent import MentorSkillRegistry
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def write_skill(root: Path, directory: str, skill_id: str, name: str) -> None:
|
||||
path = root / directory
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
(path / "SKILL.md").write_text(
|
||||
"\n".join(
|
||||
(
|
||||
"---",
|
||||
f"name: {skill_id}",
|
||||
"description: |",
|
||||
f" {name}的思维框架。",
|
||||
f" 用途:使用{name}的视角分析市场。",
|
||||
"---",
|
||||
"",
|
||||
f"# {name} · 思维操作系统",
|
||||
"",
|
||||
'> "先看事实,再做判断。"',
|
||||
"",
|
||||
"### 模型1: 证据优先",
|
||||
)
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
class MentorSkillRegistryTests(unittest.TestCase):
|
||||
def test_private_skills_require_explicit_inclusion(self):
|
||||
with tempfile.TemporaryDirectory() as temporary:
|
||||
base = Path(temporary)
|
||||
public_root = base / "public"
|
||||
private_root = base / "private"
|
||||
write_skill(public_root, "public-person", "public-person", "公开老师")
|
||||
write_skill(private_root, "private-person", "private-person", "私有老师")
|
||||
(public_root / "mentor_catalog.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"mentors": {
|
||||
"public-person": {
|
||||
"evidence": {"grade": "A", "label": "原始语料", "note": "原帖"},
|
||||
"quality": {"score": 6, "total": 6, "status": "pass"},
|
||||
}
|
||||
}
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
registry = MentorSkillRegistry(public_root, private_root)
|
||||
|
||||
self.assertEqual([item.skill_id for item in registry.list_skills()], ["public-person"])
|
||||
with self.assertRaises(ValueError):
|
||||
registry.get_skill("private-person")
|
||||
|
||||
admin_skills = registry.list_skills(include_private=True)
|
||||
self.assertEqual({item.skill_id for item in admin_skills}, {"public-person", "private-person"})
|
||||
private_skill = registry.get_skill("private-person", include_private=True)
|
||||
self.assertTrue(private_skill.is_private)
|
||||
self.assertTrue(private_skill.public()["private"])
|
||||
public_skill = registry.get_skill("public-person")
|
||||
self.assertEqual(public_skill.evidence_grade, "A")
|
||||
self.assertEqual(public_skill.quality_score, 6)
|
||||
|
||||
def test_all_public_project_skills_have_catalog_metadata(self):
|
||||
registry = MentorSkillRegistry(ROOT / "游资skills")
|
||||
skills = registry.list_skills()
|
||||
self.assertGreaterEqual(len(skills), 21)
|
||||
self.assertNotIn("xiaobai-perspective", {item.skill_id for item in skills})
|
||||
self.assertTrue(all(item.evidence_grade in {"A", "B", "C"} for item in skills))
|
||||
self.assertTrue(all(item.quality_total == 6 for item in skills))
|
||||
|
||||
def test_server_applies_private_guard_to_every_mentor_entry_point(self):
|
||||
source = (ROOT / "server.py").read_text(encoding="utf-8")
|
||||
mentor_section = source[source.index(" def mentor_setup"):source.index(" def _heaven_manual_schema")]
|
||||
self.assertGreaterEqual(
|
||||
mentor_section.count('include_private=self.membership()["is_admin"]'),
|
||||
4,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user