684 lines
31 KiB
Python
684 lines
31 KiB
Python
"""Tests for the prompt-assembly deduplication refactor.
|
|
|
|
These tests pin down the structural decisions made in the refactor:
|
|
|
|
1. Work-item identity (the global_intent_summary / your_responsibility /
|
|
inputs / deliverables / acceptance_criteria / out_of_scope block)
|
|
is rendered exactly ONCE in any per-turn prompt for a company-mode
|
|
role. The runtime plan and ownership contract no longer
|
|
re-render those fields.
|
|
|
|
2. Static collaboration boilerplate (Company Work Item Contract, the
|
|
Work Item Turn: ... headers, the Blocking collaboration (rare)
|
|
block, the "When to send / When to reply" prose previously baked
|
|
into the comms inbox section) no longer appears in any
|
|
ContextAssembler output. It now lives in the
|
|
``collaboration-playbook`` skill, injected per-turn through the
|
|
skills channel for company_mode runs only.
|
|
|
|
3. ``SkillLibrary.build_skills_summary`` honors the per-skill
|
|
``modes`` frontmatter field: a skill restricted to company_mode
|
|
is fully hidden — body AND description — when the call is made
|
|
without a matching ``execution_mode``.
|
|
|
|
4. ``render_inbox_section`` no longer emits the "When to send" /
|
|
"When to reply" prose. ``render_meetings_section`` returns "" when
|
|
no meetings are open instead of emitting "(no open meetings)".
|
|
|
|
The first two tests are the load-bearing ones: if a future refactor
|
|
introduces a new builder that re-renders work-item identity or
|
|
re-introduces a static collaboration paragraph into the per-turn
|
|
prompt, these tests should fail and tell the author to either move
|
|
the content into the playbook skill or pick a single canonical
|
|
section to own it.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock
|
|
|
|
from opc.core.models import SessionMessageRecord, SessionPartRecord
|
|
from opc.core.models import Task
|
|
from opc.layer1_perception.context_assembler import ContextAssembler
|
|
from opc.layer5_memory.memory_manager import MemoryManager
|
|
from opc.layer2_organization import comms as _comms
|
|
from opc.layer2_organization.collaboration_policy import render_ownership_contract
|
|
from opc.layer5_memory.skill_library import Skill, SkillLibrary
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Helpers
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
_STATIC_PHRASES_THAT_BELONG_IN_THE_PLAYBOOK = (
|
|
# Removed from context_assembler._build_work_item_collaboration_guidelines
|
|
"## Company Work Item Contract",
|
|
"## Work Item Turn: Plan / Intake / Dispatch",
|
|
"## Work Item Turn: Execute",
|
|
"## Work Item Turn: Review",
|
|
"## Work Item Turn: Aggregate / Deliver",
|
|
# Removed from build_external_runtime_mailbox_context
|
|
"## Blocking collaboration (rare)",
|
|
# Removed from comms.render_inbox_section
|
|
"### When to send a message",
|
|
"### When to reply to a message you received",
|
|
# Removed from comms.render_meetings_section empty case
|
|
"(no open meetings)",
|
|
)
|
|
|
|
|
|
def _work_item_assignment_task(*, role: str = "ceo") -> Task:
|
|
"""Build a task with a representative work_item_assignment payload."""
|
|
return Task(
|
|
id=f"task-{role}",
|
|
title="CEO Intake",
|
|
description="legacy description",
|
|
assigned_to=role,
|
|
metadata={
|
|
"execution_mode": "company_mode",
|
|
"work_item_projection_title": "CEO Intake",
|
|
"work_item_turn_type": "intake",
|
|
"work_item_assignment": {
|
|
"global_intent_summary": "Ship the migration to the new auth provider this quarter.",
|
|
"your_responsibility": "Frame the company-wide intake brief and route slices to the C-suite.",
|
|
"out_of_scope": [
|
|
"Do not implement code in this work item.",
|
|
"Do not validate gate evidence in this work item.",
|
|
],
|
|
"inputs": [
|
|
"Use the original user request as the mission baseline.",
|
|
],
|
|
"deliverables": [
|
|
"deliverables/ceo_intake.md with Mission Summary, Scope, Risks, Routing, Acceptance",
|
|
],
|
|
"acceptance_criteria": [
|
|
"Brief covers every required section and stays inside the work-item boundary.",
|
|
],
|
|
},
|
|
"work_item_runtime_plan": {
|
|
"turn_type": "intake",
|
|
"summary": "Frame the company mission and route slices.",
|
|
# The next four are intentionally duplicates of the
|
|
# work_item_assignment fields above. After the refactor
|
|
# they MUST NOT be rendered by _render_work_item_runtime_plan.
|
|
"inputs": ["Use the original user request as the mission baseline."],
|
|
"deliverables": ["deliverables/ceo_intake.md ..."],
|
|
"acceptance_criteria": ["Brief covers every required section ..."],
|
|
"out_of_scope": ["Do not implement code in this work item."],
|
|
# Work-item-unique field that the runtime plan IS allowed
|
|
# to carry. Used to verify the runtime plan still
|
|
# surfaces non-duplicate content.
|
|
"collaboration_expectations": [
|
|
"Direct messages for clarifications; meetings only for cross-role decisions."
|
|
],
|
|
"verification_required": False,
|
|
},
|
|
"ownership_contract": {
|
|
# The summary and expected_artifacts fields are
|
|
# intentionally duplicates of the work_item_assignment
|
|
# responsibility/deliverables. After the refactor
|
|
# render_ownership_contract MUST NOT render them.
|
|
"summary": "Frame the company-wide intake brief and route slices to the C-suite.",
|
|
"expected_artifacts": ["deliverables/ceo_intake.md ..."],
|
|
# The boundary fields below SHOULD still be rendered.
|
|
"write_scope": "/tmp/ceo-intake-workspace",
|
|
"allowed_collaboration_targets": ["cto"],
|
|
"downstream_consumer": ["cto"],
|
|
},
|
|
},
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Bucket 1: work-item identity is rendered exactly once
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class WorkItemIdentityDeduplicationTests(unittest.TestCase):
|
|
def test_work_item_runtime_plan_does_not_re_render_work_item_assignment_fields(self) -> None:
|
|
assembler = ContextAssembler(memory=SimpleNamespace())
|
|
task = _work_item_assignment_task()
|
|
|
|
rendered = assembler._render_work_item_runtime_plan(task.metadata["work_item_runtime_plan"])
|
|
|
|
# Headings that used to come out of work_item_runtime_plan are
|
|
# now ONLY produced by company_mode._build_work_item_description /
|
|
# ContextAssembler.build_task_brief. The runtime plan
|
|
# renderer must not re-emit them.
|
|
self.assertNotIn("Inputs:", rendered)
|
|
self.assertNotIn("Deliverables:", rendered)
|
|
self.assertNotIn("Acceptance Criteria:", rendered)
|
|
self.assertNotIn("Out of Scope:", rendered)
|
|
# Work-item-unique fields are still rendered.
|
|
self.assertIn("Collaboration Expectations:", rendered)
|
|
self.assertIn("Direct messages for clarifications", rendered)
|
|
self.assertIn("Work item turn type: intake", rendered)
|
|
self.assertIn("No automatic verification pass is required", rendered)
|
|
|
|
def test_render_ownership_contract_omits_summary_and_expected_artifacts(self) -> None:
|
|
task = _work_item_assignment_task()
|
|
|
|
rendered = render_ownership_contract(task)
|
|
|
|
# The boundary header still appears...
|
|
self.assertIn("## Ownership Contract", rendered)
|
|
self.assertIn("Write scope: /tmp/ceo-intake-workspace", rendered)
|
|
self.assertIn("Allowed collaboration targets:", rendered)
|
|
self.assertIn("Downstream consumers:", rendered)
|
|
# ...but the summary and expected_artifacts strings (which
|
|
# are duplicates of work-item identity) MUST NOT.
|
|
self.assertNotIn("Frame the company-wide intake brief", rendered)
|
|
self.assertNotIn("Expected artifacts:", rendered)
|
|
self.assertNotIn("deliverables/ceo_intake.md ...", rendered)
|
|
|
|
def test_build_task_brief_renders_projection_identity_once(self) -> None:
|
|
assembler = ContextAssembler(memory=SimpleNamespace())
|
|
task = _work_item_assignment_task()
|
|
|
|
brief = assembler.build_task_brief(task)
|
|
|
|
# Work-item identity headers appear exactly once.
|
|
self.assertEqual(brief.count("## Global Intent Summary"), 1)
|
|
self.assertEqual(brief.count("## Your Responsibility"), 1)
|
|
self.assertEqual(brief.count("## Inputs"), 1)
|
|
self.assertEqual(brief.count("## Deliverables"), 1)
|
|
self.assertEqual(brief.count("## Acceptance Criteria"), 1)
|
|
self.assertEqual(brief.count("## Out of Scope"), 1)
|
|
# The literal responsibility text from work_item_assignment also
|
|
# appears exactly once across the entire brief.
|
|
self.assertEqual(
|
|
brief.count("Frame the company-wide intake brief and route slices to the C-suite."),
|
|
1,
|
|
)
|
|
# runtime role map is no longer included in build_task_brief
|
|
# — it was superseded by the unified Topology section in the
|
|
# collaboration context path. Pinning this down so a future
|
|
# refactor doesn't accidentally re-introduce it.
|
|
self.assertNotIn("## Runtime Role Map", brief)
|
|
# Ownership contract is also no longer included in
|
|
# build_task_brief — it lives only in the collaboration
|
|
# path now, so the brief and the system context don't
|
|
# double-render it for native agents.
|
|
self.assertNotIn("## Ownership Contract", brief)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Bootstrap work-item assignment input/acceptance deduplication
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class RuntimeWorkItemAcceptanceDedupTests(unittest.TestCase):
|
|
"""Pin down generic acceptance criteria deduplication."""
|
|
|
|
def _planner(self) -> Any:
|
|
from opc.layer2_organization.company_mode import CompanyRuntimeSpecBuilder
|
|
|
|
class _OrgEngine:
|
|
def get_agent(self, role_id: str):
|
|
return None
|
|
|
|
return CompanyRuntimeSpecBuilder(_OrgEngine())
|
|
|
|
def _projection_spec(self, *, dependencies: list[str]) -> Any:
|
|
from opc.layer2_organization.org_work_item_planner import WorkItemProjectionSpec
|
|
|
|
return WorkItemProjectionSpec(
|
|
projection_id="cto_synthesis",
|
|
turn_type="aggregate",
|
|
title="CTO Synthesis",
|
|
summary="Review child outputs and synthesize the engineering handoff.",
|
|
role_id="cto",
|
|
dependency_projection_ids=list(dependencies),
|
|
execution_strategy="auto",
|
|
)
|
|
|
|
def test_generic_acceptance_criteria_omit_main_deliverable_echo(self) -> None:
|
|
planner = self._planner()
|
|
projection_spec = self._projection_spec(dependencies=["senior_engineer__execute"])
|
|
deliverables = [
|
|
"A concrete output that fulfills this work item objective and can be handed off downstream.",
|
|
]
|
|
|
|
criteria = planner._infer_work_item_acceptance(projection_spec, deliverables)
|
|
|
|
self.assertFalse(
|
|
any(item.startswith("The main deliverable is present:") for item in criteria),
|
|
msg=f"Generic acceptance criteria still echo the first deliverable verbatim: {criteria}",
|
|
)
|
|
self.assertNotIn(deliverables[0], "\n".join(criteria))
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Bucket 2: Self section consolidation
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class SelfSectionConsolidationTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_self_section_merges_identity_persona_and_delta(self) -> None:
|
|
memory = SimpleNamespace(
|
|
build_agent_memory_context=AsyncMock(return_value=""),
|
|
build_memory_context=AsyncMock(return_value=""),
|
|
)
|
|
assembler = ContextAssembler(memory=memory, store=SimpleNamespace())
|
|
task = Task(
|
|
id="self-section-task",
|
|
title="Engineering",
|
|
assigned_to="senior_engineer",
|
|
metadata={
|
|
"execution_mode": "company_mode",
|
|
"employee_assignment": {
|
|
"name": "Backend Architect",
|
|
"employee_id": "backend-architect",
|
|
"role_id": "senior_engineer",
|
|
"category": "engineering",
|
|
"domains": ["coding", "api"],
|
|
"experience_score": 5,
|
|
},
|
|
"employee_prompt_context": "Backend specialist focused on APIs.",
|
|
"employee_delta_context": "## Working Patterns\n- Leave reviewer-friendly artifacts.",
|
|
},
|
|
)
|
|
|
|
rendered = await assembler.build_role_reference_context(task, role_id="senior_engineer")
|
|
|
|
# Single canonical Self section with role, employee, and profile sub-blocks.
|
|
self.assertIn("## Self", rendered)
|
|
self.assertIn("### Role", rendered)
|
|
self.assertIn("- Role: senior_engineer", rendered)
|
|
self.assertIn("### Employee", rendered)
|
|
self.assertIn("- Employee: Backend Architect", rendered)
|
|
self.assertIn("### Employee Persona", rendered)
|
|
self.assertIn("Backend specialist focused on APIs.", rendered)
|
|
self.assertIn("### Learned Working Profile", rendered)
|
|
# The legacy independent headers must NOT be present.
|
|
self.assertNotIn("## Assigned Employee", rendered)
|
|
self.assertNotRegex(rendered, r"(?m)^## Employee Persona$")
|
|
self.assertNotIn("## Employee Delta Profile", rendered)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Static collaboration boilerplate is gone from per-turn prompt
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class StaticCollaborationBoilerplateRemovedTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_no_static_collaboration_phrases_in_external_context(self) -> None:
|
|
memory = SimpleNamespace(
|
|
build_focused_memory_context=AsyncMock(return_value=""),
|
|
build_memory_context=AsyncMock(return_value=""),
|
|
build_agent_memory_context=AsyncMock(return_value=""),
|
|
)
|
|
# We do not need a real CommunicationManager — just enough
|
|
# surface for build_collaboration_context to short-circuit.
|
|
communication = SimpleNamespace(
|
|
build_agent_context=AsyncMock(
|
|
return_value={
|
|
"inbox": [],
|
|
"annotations": [],
|
|
"pending_handoffs": [],
|
|
"allowed_contacts": [
|
|
{
|
|
"role_id": "cto",
|
|
"name": "CTO",
|
|
"relation": "downstream",
|
|
"responsibility": "Owns technical execution",
|
|
}
|
|
],
|
|
}
|
|
)
|
|
)
|
|
assembler = ContextAssembler(memory=memory, store=None, communication=communication)
|
|
task = _work_item_assignment_task()
|
|
|
|
external_ctx = await assembler.build_external_context(task, role_id="ceo")
|
|
|
|
for phrase in _STATIC_PHRASES_THAT_BELONG_IN_THE_PLAYBOOK:
|
|
self.assertNotIn(
|
|
phrase,
|
|
external_ctx,
|
|
msg=f"Static phrase {phrase!r} should now live in the collaboration-playbook skill, not in per-turn context",
|
|
)
|
|
|
|
async def test_task_mode_external_context_omits_collaboration_mailbox_and_comms_prompting(self) -> None:
|
|
memory = SimpleNamespace(
|
|
build_focused_memory_context=AsyncMock(return_value=""),
|
|
build_memory_context=AsyncMock(return_value=""),
|
|
build_agent_memory_context=AsyncMock(return_value=""),
|
|
)
|
|
assembler = ContextAssembler(
|
|
memory=memory,
|
|
store=None,
|
|
communication=SimpleNamespace(build_agent_context=AsyncMock(return_value={})),
|
|
)
|
|
task = Task(
|
|
id="task-mode-external",
|
|
title="Task Mode External",
|
|
description="Handle the task directly.",
|
|
assigned_to="task_generalist",
|
|
metadata={
|
|
"mode": "task",
|
|
"execution_mode": "task_mode",
|
|
"work_item_role_id": "task_generalist",
|
|
},
|
|
context_snapshot={},
|
|
)
|
|
|
|
external_ctx = await assembler.build_external_context(task, role_id="task_generalist")
|
|
|
|
self.assertNotIn("## Continue After Blocking Collaboration", external_ctx)
|
|
self.assertNotIn("## External Collaboration Actions", external_ctx)
|
|
self.assertNotIn("## Comms", external_ctx)
|
|
|
|
def test_render_inbox_section_omits_when_to_send_prose(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
layout = _comms.resolve_layout(tmpdir, "proj1", "sess1")
|
|
_comms.ensure_layout(layout, ["ceo", "cto"])
|
|
rendered = _comms.render_inbox_section(layout, "ceo")
|
|
self.assertIn("### Mailbox", rendered)
|
|
self.assertIn("runtime-owned", rendered)
|
|
# The standing rules now live in the playbook skill.
|
|
self.assertNotIn("### When to send a message", rendered)
|
|
self.assertNotIn("### When to reply to a message you received", rendered)
|
|
self.assertNotIn("Send ONLY when", rendered)
|
|
self.assertNotIn("Reply ONLY when", rendered)
|
|
self.assertNotIn("call `read_inbox`", rendered)
|
|
|
|
def test_render_meetings_section_returns_empty_when_no_meetings(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
layout = _comms.resolve_layout(tmpdir, "proj1", "sess1")
|
|
_comms.ensure_layout(layout, ["ceo"])
|
|
rendered = _comms.render_meetings_section(layout, "ceo")
|
|
self.assertEqual(rendered, "")
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Current-turn replay is omitted from prompt history
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class PromptHistoryReplayTests(unittest.IsolatedAsyncioTestCase):
|
|
async def test_session_prompt_builders_skip_latest_user_turn_when_requested(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
memory = MemoryManager(Path(tmpdir))
|
|
earlier_user = SessionMessageRecord(session_id="sess-1", role="user")
|
|
assistant = SessionMessageRecord(session_id="sess-1", role="assistant")
|
|
current_user = SessionMessageRecord(session_id="sess-1", role="user")
|
|
visible_items = [
|
|
{
|
|
"message": earlier_user,
|
|
"parts": [SessionPartRecord(message_id=earlier_user.message_id, session_id="sess-1", payload={"text": "first request"})],
|
|
},
|
|
{
|
|
"message": assistant,
|
|
"parts": [SessionPartRecord(message_id=assistant.message_id, session_id="sess-1", payload={"text": "assistant reply"})],
|
|
},
|
|
{
|
|
"message": current_user,
|
|
"parts": [SessionPartRecord(message_id=current_user.message_id, session_id="sess-1", payload={"text": "current request"})],
|
|
},
|
|
]
|
|
memory._get_visible_session_transcript = AsyncMock(return_value=visible_items) # type: ignore[method-assign]
|
|
memory.build_session_memory_context = AsyncMock(return_value="") # type: ignore[method-assign]
|
|
|
|
full_history = await memory.build_session_history_messages("sess-1")
|
|
trimmed_history = await memory.build_session_history_tail_messages(
|
|
"sess-1",
|
|
include_latest_user_turn=False,
|
|
)
|
|
prompt_context = await memory.build_session_prompt_context(
|
|
"sess-1",
|
|
include_latest_user_turn=False,
|
|
)
|
|
|
|
self.assertEqual(len(full_history), 3)
|
|
self.assertEqual(len(trimmed_history), 2)
|
|
self.assertIn("first request", trimmed_history[0]["content"])
|
|
self.assertIn("assistant reply", trimmed_history[1]["content"])
|
|
self.assertNotIn("current request", prompt_context)
|
|
self.assertIn("first request", prompt_context)
|
|
self.assertIn("assistant reply", prompt_context)
|
|
|
|
async def test_child_session_seed_message_is_omitted_from_prompt_history(self) -> None:
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
memory = MemoryManager(Path(tmpdir))
|
|
seed_user = SessionMessageRecord(
|
|
session_id="sess-2",
|
|
role="user",
|
|
metadata={"kind": "child_session_seed"},
|
|
)
|
|
assistant = SessionMessageRecord(session_id="sess-2", role="assistant")
|
|
followup_user = SessionMessageRecord(session_id="sess-2", role="user")
|
|
visible_items = [
|
|
{
|
|
"message": seed_user,
|
|
"parts": [
|
|
SessionPartRecord(
|
|
message_id=seed_user.message_id,
|
|
session_id="sess-2",
|
|
payload={
|
|
"text": "## Global Intent Summary\n做一个3d坦克大战游戏\n\n## Your Responsibility\nOwn the execution work item for role engineer."
|
|
},
|
|
)
|
|
],
|
|
},
|
|
{
|
|
"message": assistant,
|
|
"parts": [SessionPartRecord(message_id=assistant.message_id, session_id="sess-2", payload={"text": "working on it"})],
|
|
},
|
|
{
|
|
"message": followup_user,
|
|
"parts": [SessionPartRecord(message_id=followup_user.message_id, session_id="sess-2", payload={"text": "please keep the game browser-friendly"})],
|
|
},
|
|
]
|
|
memory._get_visible_session_transcript = AsyncMock(return_value=visible_items) # type: ignore[method-assign]
|
|
memory.build_session_memory_context = AsyncMock(return_value="") # type: ignore[method-assign]
|
|
|
|
prompt_context = await memory.build_session_prompt_context(
|
|
"sess-2",
|
|
include_latest_user_turn=False,
|
|
)
|
|
history_messages = await memory.build_session_history_messages(
|
|
"sess-2",
|
|
include_latest_user_turn=False,
|
|
)
|
|
|
|
self.assertNotIn("Your Responsibility", prompt_context)
|
|
self.assertNotIn("Global Intent Summary", prompt_context)
|
|
self.assertEqual(len(history_messages), 1)
|
|
self.assertIn("working on it", history_messages[0]["content"])
|
|
self.assertIn("working on it", prompt_context)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# SkillLibrary mode filtering and the playbook skill
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class SkillModeFilteringTests(unittest.TestCase):
|
|
def _make_library(self, *, with_playbook: bool = True) -> SkillLibrary:
|
|
tmpdir = Path(tempfile.mkdtemp(prefix="opc-skill-test-"))
|
|
skills_dir = tmpdir / "skills"
|
|
skills_dir.mkdir()
|
|
if with_playbook:
|
|
playbook_dir = skills_dir / "collaboration-playbook"
|
|
playbook_dir.mkdir()
|
|
(playbook_dir / "SKILL.md").write_text(
|
|
"---\n"
|
|
"name: collaboration-playbook\n"
|
|
"description: Standing rules for company-mode collaboration.\n"
|
|
"always: true\n"
|
|
"modes:\n"
|
|
" - company_mode\n"
|
|
"---\n\n"
|
|
"# Collaboration Playbook\n\n"
|
|
"Stay inside your work-item boundary.\n"
|
|
)
|
|
memory_dir = skills_dir / "memory"
|
|
memory_dir.mkdir()
|
|
(memory_dir / "SKILL.md").write_text(
|
|
"---\n"
|
|
"name: memory\n"
|
|
"description: Read and edit canonical OpenOPC memory.\n"
|
|
"always: true\n"
|
|
"---\n\n"
|
|
"# Memory\n\n"
|
|
"Use `.opc/memory/global.md` and `.opc/memory/projects/<current_project_id>.md`.\n"
|
|
)
|
|
# Also create one ordinary skill that should always show.
|
|
ordinary_dir = skills_dir / "writing"
|
|
ordinary_dir.mkdir()
|
|
(ordinary_dir / "SKILL.md").write_text(
|
|
"---\n"
|
|
"name: writing\n"
|
|
"description: Helpful writing patterns.\n"
|
|
"---\n\n"
|
|
"# Writing\n"
|
|
)
|
|
library = SkillLibrary(tmpdir)
|
|
library.load_all()
|
|
return library
|
|
|
|
def test_company_mode_skill_is_visible_under_company_mode(self) -> None:
|
|
library = self._make_library()
|
|
summary = library.build_skills_summary(execution_mode="company_mode")
|
|
self.assertIn("## Skill: collaboration-playbook", summary)
|
|
self.assertIn("Stay inside your work-item boundary.", summary)
|
|
# Ordinary skill is also catalogued.
|
|
self.assertIn("**writing**", summary)
|
|
|
|
def test_company_mode_skill_is_completely_hidden_in_task_mode(self) -> None:
|
|
library = self._make_library()
|
|
summary = library.build_skills_summary(execution_mode="task_mode")
|
|
# Body must be gone.
|
|
self.assertNotIn("Stay inside your work-item boundary.", summary)
|
|
self.assertNotIn("## Skill: collaboration-playbook", summary)
|
|
# Description must ALSO be gone — even the catalog summary
|
|
# entry must not appear in task mode.
|
|
self.assertNotIn("collaboration-playbook", summary)
|
|
self.assertNotIn(
|
|
"Standing rules for company-mode collaboration.", summary
|
|
)
|
|
# The ordinary skill is unaffected.
|
|
self.assertIn("**writing**", summary)
|
|
self.assertIn("## Skill: memory", summary)
|
|
|
|
def test_company_mode_skill_is_hidden_when_execution_mode_unknown(self) -> None:
|
|
library = self._make_library()
|
|
summary = library.build_skills_summary(execution_mode=None)
|
|
self.assertNotIn("collaboration-playbook", summary)
|
|
self.assertIn("**writing**", summary)
|
|
self.assertNotIn("## Skill: memory", summary)
|
|
|
|
def test_memory_skill_visible_in_task_mode_for_all_agents(self) -> None:
|
|
library = self._make_library()
|
|
summary = library.build_skills_summary(
|
|
execution_mode="task_mode",
|
|
role_id="worker",
|
|
user_facing=False,
|
|
final_decider_role_id=None,
|
|
)
|
|
self.assertIn("## Skill: memory", summary)
|
|
|
|
def test_memory_skill_visible_only_to_user_facing_final_decider_in_company_mode(self) -> None:
|
|
library = self._make_library()
|
|
worker_summary = library.build_skills_summary(
|
|
execution_mode="company_mode",
|
|
role_id="worker",
|
|
user_facing=True,
|
|
final_decider_role_id="ceo",
|
|
)
|
|
agent_turn_summary = library.build_skills_summary(
|
|
execution_mode="company_mode",
|
|
role_id="ceo",
|
|
user_facing=False,
|
|
final_decider_role_id="ceo",
|
|
)
|
|
final_summary = library.build_skills_summary(
|
|
execution_mode="company_mode",
|
|
role_id="ceo",
|
|
user_facing=True,
|
|
final_decider_role_id="ceo",
|
|
)
|
|
|
|
self.assertNotIn("## Skill: memory", worker_summary)
|
|
self.assertNotIn("## Skill: memory", agent_turn_summary)
|
|
self.assertIn("## Skill: memory", final_summary)
|
|
|
|
def test_skill_visibility_helper_handles_modes_field_shapes(self) -> None:
|
|
# Backward compat: skill with no modes is visible everywhere.
|
|
skill_no_modes = Skill(name="x", modes=[])
|
|
self.assertTrue(SkillLibrary._skill_visible_in_mode(skill_no_modes, None))
|
|
self.assertTrue(SkillLibrary._skill_visible_in_mode(skill_no_modes, "company_mode"))
|
|
self.assertTrue(SkillLibrary._skill_visible_in_mode(skill_no_modes, "task_mode"))
|
|
# Restricted skill is hidden everywhere except declared modes.
|
|
skill_company = Skill(name="y", modes=["company_mode"])
|
|
self.assertFalse(SkillLibrary._skill_visible_in_mode(skill_company, None))
|
|
self.assertFalse(SkillLibrary._skill_visible_in_mode(skill_company, ""))
|
|
self.assertFalse(SkillLibrary._skill_visible_in_mode(skill_company, "task_mode"))
|
|
self.assertTrue(SkillLibrary._skill_visible_in_mode(skill_company, "company_mode"))
|
|
skill_memory = Skill(name="memory", modes=[])
|
|
self.assertTrue(SkillLibrary._skill_visible_in_mode(skill_memory, "task_mode"))
|
|
self.assertFalse(SkillLibrary._skill_visible_in_mode(skill_memory, "company_mode"))
|
|
self.assertTrue(
|
|
SkillLibrary._skill_visible_in_mode(
|
|
skill_memory,
|
|
"company_mode",
|
|
role_id="ceo",
|
|
user_facing=True,
|
|
final_decider_role_id="ceo",
|
|
)
|
|
)
|
|
|
|
|
|
# ----------------------------------------------------------------------
|
|
# Real bundled collaboration-playbook skill
|
|
# ----------------------------------------------------------------------
|
|
|
|
|
|
class BundledPlaybookSkillTests(unittest.TestCase):
|
|
"""Sanity-check the playbook shipped under .opc/skills/."""
|
|
|
|
@classmethod
|
|
def setUpClass(cls) -> None:
|
|
# tests/ -> repo root
|
|
cls.repo_root = Path(__file__).resolve().parents[1]
|
|
cls.skill_path = cls.repo_root / ".opc" / "skills" / "collaboration-playbook" / "SKILL.md"
|
|
|
|
def test_playbook_file_exists_with_company_mode_only_frontmatter(self) -> None:
|
|
self.assertTrue(
|
|
self.skill_path.exists(),
|
|
msg=f"Expected collaboration playbook at {self.skill_path}",
|
|
)
|
|
text = self.skill_path.read_text(encoding="utf-8")
|
|
self.assertIn("name: collaboration-playbook", text)
|
|
self.assertIn("always: true", text)
|
|
# The mode restriction is what makes the skill default-true
|
|
# in company mode and invisible in task mode.
|
|
self.assertIn("modes:", text)
|
|
self.assertIn("- company_mode", text)
|
|
|
|
def test_bundled_playbook_loads_through_skill_library(self) -> None:
|
|
library = SkillLibrary(self.repo_root / ".opc")
|
|
library.load_all()
|
|
skill = library.get("collaboration-playbook")
|
|
self.assertIsNotNone(skill)
|
|
assert skill is not None
|
|
self.assertTrue(skill.always)
|
|
self.assertEqual(skill.modes, ["company_mode"])
|
|
self.assertIn("Stay inside your work-item boundary.", skill.content)
|
|
|
|
company_summary = library.build_skills_summary(execution_mode="company_mode")
|
|
self.assertIn("## Skill: collaboration-playbook", company_summary)
|
|
|
|
task_summary = library.build_skills_summary(execution_mode="task_mode")
|
|
self.assertNotIn("collaboration-playbook", task_summary)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|