Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 86 additions & 0 deletions backend/app/api/mock.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,11 +104,37 @@
}
}

EXPENSE_QUOTA_ROSTER = {
"E1001": {
"employee_name": "李明",
"department": "销售部",
"total_quota": Decimal("20000.00"),
"used": Decimal("6350.00"),
},
"E1002": {
"employee_name": "王芳",
"department": "市场部",
"total_quota": Decimal("15000.00"),
"used": Decimal("0.00"),
},
"E1003": {
"employee_name": "赵磊",
"department": "研发部",
"total_quota": Decimal("8000.00"),
"used": Decimal("8000.00"),
},
}


class MockOrderQueryRequest(BaseModel):
order_id: str


class MockExpenseQuotaQueryRequest(BaseModel):
employee_id: str
month: str | None = None


class MockOrderRefundRequest(BaseModel):
order_id: str
refund_reason: str
Expand Down Expand Up @@ -176,6 +202,35 @@ def mock_order_archive_query(request: MockOrderQueryRequest) -> dict[str, Any]:
return _order_hit(order_id, "archive_order_center", record)


@router.post("/expense/quota_query")
def mock_expense_quota_query(request: MockExpenseQuotaQueryRequest) -> dict[str, Any]:
employee_id = _normalize_employee_id(request.employee_id)
month = _normalize_month(request.month)
if not employee_id:
return _employee_miss(request.employee_id, month, "employee_id_required")
record = EXPENSE_QUOTA_ROSTER.get(employee_id)
if not record:
return _employee_miss(employee_id, month, "employee_not_found")
remaining = record["total_quota"] - record["used"]
return {
"found": True,
"source": "mock_expense_quota_roster",
"employee_id": employee_id,
"employee_name": record["employee_name"],
"department": record["department"],
"month": month,
"total_quota": float(record["total_quota"]),
"used": float(record["used"]),
"remaining": float(remaining),
"currency": "CNY",
"message": (
f"工号 {employee_id}({record['employee_name']}){month} 报销总额度 "
f"{record['total_quota']:.2f} CNY,已用 {record['used']:.2f} CNY,"
f"剩余 {remaining:.2f} CNY。"
),
}


@router.post("/order/refund")
def mock_order_refund(
request: MockOrderRefundRequest, db: Session = Depends(get_session)
Expand Down Expand Up @@ -477,6 +532,37 @@ def _order_miss(order_id: str, source: str) -> dict[str, Any]:
}


def _normalize_employee_id(value: str) -> str:
normalized = _normalize_id(value)
# Demo roster IDs are E-prefixed; accept the bare number form users often type.
if normalized.isdigit():
return f"E{normalized}"
return normalized


def _normalize_month(value: str | None) -> str:
text = (value or "").strip()
if len(text) >= 7 and text[4] == "-" and text[:4].isdigit() and text[5:7].isdigit():
return text[:7]
return datetime.now(UTC).strftime("%Y-%m")


def _employee_miss(employee_id: str, month: str, miss_reason: str) -> dict[str, Any]:
if miss_reason == "employee_id_required":
message = "未提供员工工号,无法查询报销额度。"
else:
message = f"未找到工号为 {employee_id} 的员工记录,无法查询报销额度。"
return {
"found": False,
"source": "mock_expense_quota_roster",
"employee_id": employee_id,
"month": month,
"miss_reason": miss_reason,
"message": message,
"hint": "可尝试使用 E1001、E1002 或 E1003 作为 mock 员工工号。",
}


def _find_dynamic_order(db: object, order_id: str) -> dict[str, Any] | None:
if not isinstance(db, Session):
return None
Expand Down

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions backend/app/llm/prompts/harness_agent_prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ prior_task_results 还可能包含由当前 Slot 中标识符精确引用的、
`arguments.path`,禁止按标点解析、截断、改写扩展名或自行拼接路径;返回 truncated=true 时,把返回的
`continuation_token` 和同一个 path 原样传给下一次 read_file,禁止猜测 byte offset;
不得猜测未读取内容,也不得要求系统生成额外摘要或 Schema。
- 工具结果中的 `found=false` 或 `miss_reason`(含 `employee_not_found`、`source_miss` 等)表示
“未命中”,不是查询成功。此时不得把返回的默认额度、占位标识符(如 `UNKNOWN`)或用户自行提供的
姓名、工号当作已核实的业务事实对外展示,应明确告知用户未找到对应记录;只有在 `found=true`
时才能输出工具返回的业务数据。
- 如果后续 Tool 需要完整的前序大 JSON,把该 `sandbox_json_file` 引用对象原样放入对应
参数,Harness 会在执行 Tool 前自动、安全地解引用,并按下游 input schema 还原成 JSON
object、array 或完整 JSON 字符串;不要把 JSON 手工复制回参数。
Expand Down
34 changes: 34 additions & 0 deletions backend/tests/test_mock_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,37 @@ def test_mock_api_accepts_internal_service_token() -> None:

assert response.status_code == 200
assert response.json()["found"] is True


def test_mock_expense_quota_query_requires_internal_token() -> None:
app = FastAPI()
app.include_router(router)
client = TestClient(app)
payload = {"employee_id": "E1001", "month": "2026-09"}

assert client.post("/api/mock/expense/quota_query", json=payload).status_code == 401
response = client.post(
"/api/mock/expense/quota_query",
json=payload,
headers={INTERNAL_SERVICE_HEADER: internal_service_token()},
)
assert response.status_code == 200
assert response.json()["found"] is True


def test_mock_expense_quota_query_unknown_employee_via_http() -> None:
app = FastAPI()
app.include_router(router)
client = TestClient(app)

response = client.post(
"/api/mock/expense/quota_query",
json={"employee_id": "666", "month": "2026-09"},
headers={INTERNAL_SERVICE_HEADER: internal_service_token()},
)

assert response.status_code == 200
body = response.json()
assert body["found"] is False
assert body["miss_reason"] == "employee_not_found"
assert "total_quota" not in body
56 changes: 56 additions & 0 deletions backend/tests/test_mock_expense_quota_query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from app.api.mock import (
MockExpenseQuotaQueryRequest,
mock_expense_quota_query,
)


def test_expense_quota_query_returns_found_employee() -> None:
result = mock_expense_quota_query(
MockExpenseQuotaQueryRequest(employee_id="E1001", month="2026-09")
)

assert result["found"] is True
assert result["employee_name"] == "李明"
assert result["department"] == "销售部"
assert result["month"] == "2026-09"
assert result["total_quota"] == 20000.0
assert result["used"] == 6350.0
assert result["remaining"] == 13650.0


def test_expense_quota_query_accepts_bare_numeric_employee_id() -> None:
result = mock_expense_quota_query(
MockExpenseQuotaQueryRequest(employee_id="1001", month="2026-09")
)

assert result["found"] is True
assert result["employee_id"] == "E1001"


def test_expense_quota_query_returns_miss_for_unknown_employee() -> None:
result = mock_expense_quota_query(
MockExpenseQuotaQueryRequest(employee_id="999999", month="2026-09")
)

assert result["found"] is False
assert result["miss_reason"] == "employee_not_found"
assert "total_quota" not in result
assert "remaining" not in result
assert "999999" in result["message"]


def test_expense_quota_query_returns_miss_when_employee_id_missing() -> None:
result = mock_expense_quota_query(
MockExpenseQuotaQueryRequest(employee_id=" ", month="2026-09")
)

assert result["found"] is False
assert result["miss_reason"] == "employee_id_required"
assert "total_quota" not in result


def test_expense_quota_query_defaults_month_to_current() -> None:
result = mock_expense_quota_query(MockExpenseQuotaQueryRequest(employee_id="E1002"))

assert result["found"] is True
assert result["month"].endswith("-09") or result["month"].endswith("-10")
32 changes: 32 additions & 0 deletions backend/tests/test_staffdeck_seed.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,38 @@ def test_staffdeck_seed_adds_expanded_employee_profiles_idempotently() -> None:
) == 1


def test_staffdeck_seed_expense_quota_tool_handles_missing_employees() -> None:
with _seeded_session() as db:
quota_tool = db.exec(
select(Tool).where(
Tool.tenant_id == "tenant_demo",
Tool.name == "expense.quota_query",
)
).one()
quota_skill = db.exec(
select(Skill).where(
Skill.tenant_id == "tenant_demo",
Skill.skill_id == "skill_expense_quota_query",
)
).one()

# The tool runs against the in-repo mock so unknown employees yield a miss
# contract instead of an external demo returning fabricated quotas.
assert quota_tool.url == "/api/mock/expense/quota_query"
output_schema = quota_tool.output_schema
assert output_schema["properties"]["found"]["type"] == "boolean"
assert "miss_reason" in output_schema["properties"]

# The response step must treat found=false as "no such employee" and must
# not display quota numbers for it.
nodes = {node["node_id"]: node for node in quota_skill.content_json["nodes"]}
response_instruction = nodes["node_response_result"]["instruction"]
assert "found=false" in response_instruction
assert "不得展示任何额度数字" in response_instruction
collect_instruction = nodes["node_collect_info"]["instruction"]
assert "姓名不能替代工号" in collect_instruction


def test_staffdeck_seed_applies_reliability_defaults_to_existing_rows() -> None:
with _seeded_session() as db:
archive_tool = db.exec(
Expand Down