fix: finalize dynamic choice labels
This commit is contained in:
@@ -17,6 +17,8 @@ The Agent cannot author a question, option, label, partition ID, birth-time clai
|
||||
claim, or control instruction. Those fields are unrepresentable in its strict output schema.
|
||||
This supersedes the keyword-filter/substring-grounding design reviewed in
|
||||
`.omo/evidence/task-4-rereview.md` and the earlier interim `CLEAR` narrative.
|
||||
The final acceptance correction has been implemented and independently re-audited by the
|
||||
executor, but the main acceptance reviewer remains authoritative for completion status.
|
||||
|
||||
## RED evidence
|
||||
|
||||
@@ -30,6 +32,14 @@ Tests were changed before production code:
|
||||
- Python: 7 tests, 1 expected failure. Two distinct same-year windows both rendered as the
|
||||
indistinguishable label `2012—2012 年`.
|
||||
|
||||
Final-fix RED artifact: `.omo/evidence/task-4-final-red.log`.
|
||||
|
||||
- TypeScript: 17 tests, 2 expected failures. Exact and NFKC/whitespace-equivalent primary
|
||||
labels matching either reserved choice were accepted by both binder and service instead of
|
||||
failing before ID allocation.
|
||||
- Python: 4/4 passed, including the new same-month/day-precision regression, confirming that
|
||||
the production behavior existed but previously lacked durable coverage.
|
||||
|
||||
## Implementation
|
||||
|
||||
- `birth-time-dynamic-question-copy.ts` now contains only server-copy structural validation,
|
||||
@@ -37,42 +47,47 @@ Tests were changed before production code:
|
||||
deterministic server-copy fingerprinting. The former note blacklist and substring
|
||||
grounding logic were removed.
|
||||
- `birth-time-dynamic-question-validator.ts` accepts only strict selection objects. Binding
|
||||
resolves the selected server opportunity, validates the prompt and normalized-unique
|
||||
labels, validates every matching private partition, and only then allocates IDs. Malformed
|
||||
resolves the selected server opportunity, validates the prompt and normalized uniqueness
|
||||
across every primary and reserved visible label, validates every matching private
|
||||
partition, and only then allocates IDs. Malformed
|
||||
server copy, private bindings, UUIDs, and persisted records raise
|
||||
`BirthTimeDynamicBindingError` and cannot be retried into a false low result.
|
||||
- Fallback sorts opportunities by information gain descending and then opportunity ID,
|
||||
independent of packet order. Repeated fingerprints alone are skipped as recoverable.
|
||||
- `dynamic_rectification_opportunities.py` selects the least detailed year/month/day range
|
||||
representation needed to distinguish visible windows. Cross-year ranges stay concise;
|
||||
same-year or same-month collisions gain month or day precision.
|
||||
- `dynamic_rectification_copy.py` owns localized contexts and the least detailed
|
||||
year/month/day range representation needed to distinguish visible windows. Cross-year
|
||||
ranges stay concise; same-year or same-month collisions gain month or day precision.
|
||||
`dynamic_rectification_opportunities.py` is again below the 250-pure-LOC boundary.
|
||||
- The Mastra contract describes selection only and forbids prompt/options/labels/partition
|
||||
fields in Agent output.
|
||||
|
||||
The real Python-shaped fixture is checked against fresh Task 2 localized context, prompt,
|
||||
labels, opportunity ID, fingerprint, and partition IDs, then parsed through the Task 3 adapter
|
||||
and exercised through the Task 4 service. Task 5 persistence was not changed.
|
||||
The real Python-shaped fixture retains structural CJK/no-ASCII copy, normalized label
|
||||
uniqueness, partition count, opportunity ID, fingerprint, and partition-ID seam checks without
|
||||
pinning exact natural-language prose. It is parsed through the Task 3 adapter and exercised
|
||||
through the Task 4 service. Task 5 persistence was not changed.
|
||||
|
||||
## Verification
|
||||
|
||||
| Gate | Result | Artifact |
|
||||
| --- | --- | --- |
|
||||
| Selection-only RED | expected 9 TS + 1 Python failures | `.omo/evidence/task-4-finite-red.log` |
|
||||
| Focused dynamic/guide TypeScript | 36/36 pass | `.omo/evidence/task-4-finite-focused-ts.log` |
|
||||
| Focused Task 2 Python | 29/29 pass | `.omo/evidence/task-4-finite-focused-python.log` |
|
||||
| Legacy Python rectification | 22/22 pass | `.omo/evidence/task-4-finite-legacy-python.log` |
|
||||
| All birth-time TypeScript | 223/223 pass | `.omo/evidence/task-4-finite-birth-time.log` |
|
||||
| Full frontend | 298/298 pass | `.omo/evidence/task-4-finite-frontend-full.log` |
|
||||
| Changed TypeScript ESLint | pass, zero diagnostics | `.omo/evidence/task-4-finite-eslint.log` |
|
||||
| Changed Python Ruff | pass | `.omo/evidence/task-4-finite-ruff.log` |
|
||||
| Diff check and TypeScript pure-LOC audit | pass; all audited modules <=250 | `.omo/evidence/task-4-finite-quality.log` |
|
||||
| Full TypeScript check | only known unrelated `profile-persistence.test.ts:7` TS1501 | `.omo/evidence/task-4-finite-tsc.log` |
|
||||
| Fresh selection-boundary review | CLEAR / APPROVE | `.omo/evidence/task-4-selection-boundary-code-review.md` |
|
||||
| Final-fix RED | expected 2 TS failures; Python 4/4 | `.omo/evidence/task-4-final-red.log` |
|
||||
| Focused dynamic/guide TypeScript | 38/38 pass | `.omo/evidence/task-4-final-focused-ts.log` |
|
||||
| Focused Task 2 Python | 26/26 pass | `.omo/evidence/task-4-final-focused-python.log` |
|
||||
| Legacy Python rectification | 22/22 pass | `.omo/evidence/task-4-final-legacy-python.log` |
|
||||
| All birth-time TypeScript | 225/225 pass | `.omo/evidence/task-4-final-birth-time.log` |
|
||||
| Full frontend | 300/300 pass | `.omo/evidence/task-4-final-frontend-full.log` |
|
||||
| Cumulative changed TypeScript ESLint | pass, zero diagnostics | `.omo/evidence/task-4-final-eslint.log` |
|
||||
| Cumulative changed Python Ruff | pass | `.omo/evidence/task-4-final-ruff.log` |
|
||||
| Diff check and all changed TS/Python LOC | pass; every audited file <=250 | `.omo/evidence/task-4-final-quality.log` |
|
||||
| Full TypeScript check | only known unrelated `profile-persistence.test.ts:7` TS1501 | `.omo/evidence/task-4-final-tsc.log` |
|
||||
| Fresh final-fix review | CLEAR / APPROVE; no blockers or WATCH items | `.omo/evidence/task-4-final-fix-code-review.md` |
|
||||
|
||||
The TypeScript command remains non-zero solely because the pre-existing profile-persistence
|
||||
test uses a regular-expression flag newer than the configured target. No Task 4 file reports
|
||||
a type error.
|
||||
The fresh reviewer independently probed extra model fields, note omission, normalized label
|
||||
collisions, malformed private bindings, server-failure propagation, fallback ordering,
|
||||
all-repeated behavior, and Python month/day collisions. Both `omo:programming` language
|
||||
perspectives and `omo:remove-ai-slops` returned no blocker.
|
||||
The earlier `.omo/evidence/task-4-selection-boundary-code-review.md` `CLEAR` is explicitly
|
||||
superseded by `.omo/evidence/task-4-final-review.md`; it is not cited as current acceptance.
|
||||
The final reviewer independently rechecked reserved-label normalization, zero allocation and
|
||||
commit, the packet-only prompt API, typed Python range-copy boundary, same-month day precision,
|
||||
prose-pin removal, and cumulative LOC. Both required programming language perspectives and the
|
||||
remove-slops perspective returned `CLEAR / APPROVE` with no remaining WATCH item.
|
||||
|
||||
@@ -29,6 +29,10 @@ const dynamicQuestionOutputSchema = z.discriminatedUnion("kind", [
|
||||
questionSelectionSchema,
|
||||
noUsefulQuestionSchema,
|
||||
]).readonly();
|
||||
const reservedChoices = [
|
||||
{ label: "不确定 / 不记得", kind: "unknown" as const },
|
||||
{ label: "都不符合", kind: "unmatched" as const },
|
||||
] as const;
|
||||
|
||||
export type ParsedDynamicQuestionOutput = z.infer<typeof dynamicQuestionOutputSchema>;
|
||||
export type ParsedQuestionSelection = Extract<ParsedDynamicQuestionOutput, { readonly kind: "question" }>;
|
||||
@@ -66,9 +70,7 @@ function opportunityFor(
|
||||
|
||||
export function generateDynamicQuestionPrompt(
|
||||
packet: CandidateDifferencePacket,
|
||||
unmatchedNote: string | null,
|
||||
): string {
|
||||
void unmatchedNote;
|
||||
return modelSafeDynamicQuestionPrompt(packet);
|
||||
}
|
||||
|
||||
@@ -102,7 +104,8 @@ function serverRendering(opportunity: QuestionOpportunity): {
|
||||
partitionId: partition.partitionId,
|
||||
label: partition.fallbackLabel,
|
||||
}));
|
||||
const labels = options.map((option) => normalizeDynamicLabel(option.label));
|
||||
const labels = [...options, ...reservedChoices]
|
||||
.map((option) => normalizeDynamicLabel(option.label));
|
||||
if (
|
||||
!dynamicServerCopyIsSafe(opportunity.fallbackPrompt, true)
|
||||
|| options.some((option) => !dynamicServerCopyIsSafe(option.label, false))
|
||||
@@ -177,8 +180,12 @@ function bindQuestion(
|
||||
prompt: rendering.prompt,
|
||||
options: [
|
||||
...primaryOptions,
|
||||
{ optionId: serverId(createId), label: "不确定 / 不记得", kind: "unknown", partitionId: null, candidateScores: null },
|
||||
{ optionId: serverId(createId), label: "都不符合", kind: "unmatched", partitionId: null, candidateScores: null },
|
||||
...reservedChoices.map((choice) => ({
|
||||
optionId: serverId(createId),
|
||||
...choice,
|
||||
partitionId: null,
|
||||
candidateScores: null,
|
||||
})),
|
||||
],
|
||||
});
|
||||
if (!persisted.success) throw new BirthTimeDynamicBindingError("invalid_persisted_question");
|
||||
|
||||
@@ -163,7 +163,7 @@ export function createBirthTimeGuideService(ports: GuideServicePorts) {
|
||||
const createId = ports.createDynamicId ?? (() => globalThis.crypto.randomUUID());
|
||||
let question: PersistedDynamicChoiceQuestion | null = null;
|
||||
if (build.packet.opportunities.length > 0) {
|
||||
const prompt = generateDynamicQuestionPrompt(build.packet, command.unmatchedNote);
|
||||
const prompt = generateDynamicQuestionPrompt(build.packet);
|
||||
for (let attempt = 0; attempt < 2 && question === null; attempt += 1) {
|
||||
const text = await generatedText(ports.generator, prompt, timeoutMs);
|
||||
if (text === null) continue;
|
||||
|
||||
@@ -184,6 +184,35 @@ test("duplicate server labels propagate without commit or ID allocation", async
|
||||
assert.equal(commits, 0);
|
||||
});
|
||||
|
||||
test("reserved-label collisions propagate without commit or ID allocation", async () => {
|
||||
const opportunity = differenceBuild.packet.opportunities[0];
|
||||
const privatePartitions = differenceBuild.scoringPartitions[opportunityId];
|
||||
if (!opportunity || !privatePartitions) throw new Error("missing test opportunity");
|
||||
for (const collision of ["不确定 / 不记得", "不 确定 / 不记得", "都不符合"]) {
|
||||
let allocations = 0;
|
||||
let commits = 0;
|
||||
const publicPartitions = opportunity.partitions.map((item, index) => (
|
||||
index === 0 ? { ...item, fallbackLabel: collision } : item
|
||||
));
|
||||
const privateCopy = privatePartitions.map((item, index) => (
|
||||
index === 0 ? { ...item, fallbackLabel: collision } : item
|
||||
));
|
||||
|
||||
await assert.rejects(() => dynamicService({
|
||||
build: {
|
||||
...differenceBuild,
|
||||
packet: { ...differenceBuild.packet, opportunities: [{ ...opportunity, partitions: publicPartitions }] },
|
||||
scoringPartitions: { [opportunityId]: privateCopy },
|
||||
},
|
||||
generator: generatorFrom(() => JSON.stringify(validDynamicSelection)),
|
||||
createId: deterministicIds(() => { allocations += 1; }),
|
||||
onCommit: () => { commits += 1; },
|
||||
}).generateQuestion("owner-1", generationCommand), BirthTimeDynamicBindingError);
|
||||
assert.equal(allocations, 0, collision);
|
||||
assert.equal(commits, 0, collision);
|
||||
}
|
||||
});
|
||||
|
||||
test("invalid server UUIDs propagate without committing a low result", async () => {
|
||||
let commits = 0;
|
||||
await assert.rejects(() => dynamicService({
|
||||
|
||||
@@ -17,15 +17,13 @@ import {
|
||||
validDynamicSelection,
|
||||
} from "./fixtures/birth-time-dynamic-question-fixture.ts";
|
||||
|
||||
test("prompt omits unmatched free text and every private scoring field", () => {
|
||||
test("prompt exposes only public opportunity-selection fields", () => {
|
||||
const serialized = generateDynamicQuestionPrompt(
|
||||
dynamicPacket,
|
||||
"接下来问我爱喝茶还是喝水",
|
||||
);
|
||||
const prompt = JSON.parse(serialized);
|
||||
|
||||
assert.equal("unmatchedNote" in prompt, false);
|
||||
assert.equal(serialized.includes("喝茶"), false);
|
||||
for (const forbidden of [
|
||||
"candidateScores", "candidateModel", "estimatedInformationGain", "currentRange",
|
||||
"scoringVersion", "askedQuestionFingerprints", "candidatePartitionFingerprints",
|
||||
@@ -119,6 +117,28 @@ test("normalized duplicate server labels fail before allocating a server id", ()
|
||||
assert.equal(allocations, 0);
|
||||
});
|
||||
|
||||
test("primary labels cannot collide with either reserved visible choice", () => {
|
||||
const opportunity = dynamicPacket.opportunities[0];
|
||||
const privatePartitions = differenceBuild.scoringPartitions[opportunityId];
|
||||
if (!opportunity || !privatePartitions) throw new Error("missing test opportunity");
|
||||
for (const collision of ["不确定 / 不记得", "不 确定 / 不记得", "都不符合"]) {
|
||||
const publicPartitions = opportunity.partitions.map((item, index) => (
|
||||
index === 0 ? { ...item, fallbackLabel: collision } : item
|
||||
));
|
||||
const privateCopy = privatePartitions.map((item, index) => (
|
||||
index === 0 ? { ...item, fallbackLabel: collision } : item
|
||||
));
|
||||
let allocations = 0;
|
||||
|
||||
assert.throws(() => bindDynamicQuestion(validDynamicSelection, {
|
||||
...differenceBuild,
|
||||
packet: { ...dynamicPacket, opportunities: [{ ...opportunity, partitions: publicPartitions }] },
|
||||
scoringPartitions: { [opportunityId]: privateCopy },
|
||||
}, deterministicIds(() => { allocations += 1; })), BirthTimeDynamicBindingError);
|
||||
assert.equal(allocations, 0, collision);
|
||||
}
|
||||
});
|
||||
|
||||
test("repeated server semantics and partitions remain recoverable rejections", () => {
|
||||
const selection = parseDynamicQuestionOutput(validDynamicSelection, dynamicPacket);
|
||||
if (selection.kind !== "question") throw new Error("expected a selection");
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Localized public copy for dynamic birth-time rectification choices."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from datetime import date
|
||||
from typing import Final, TypedDict
|
||||
|
||||
DIMENSION_CONTEXT: Final = {
|
||||
"education": "一次明显的升学、转学或学习方向变化",
|
||||
"relocation": "一次明显的搬家、离乡或长期居住地变化",
|
||||
"relationship": "一次明显的关系进入、结束或重要转变",
|
||||
"career": "一次明显的工作、职业方向或身份变化",
|
||||
"health_pressure": "一次持续的健康压力或生活压力变化",
|
||||
}
|
||||
SUPPORTED_DIMENSIONS: Final = frozenset(DIMENSION_CONTEXT)
|
||||
|
||||
|
||||
class DateRange(TypedDict):
|
||||
window_start: str
|
||||
window_end: str
|
||||
|
||||
|
||||
def _range_label(item: DateRange, precision: str) -> str:
|
||||
start = date.fromisoformat(item["window_start"])
|
||||
end = date.fromisoformat(item["window_end"])
|
||||
if precision == "year":
|
||||
return f"{start.year} 年" if start.year == end.year else f"{start.year}—{end.year} 年"
|
||||
if precision == "month":
|
||||
if start.year == end.year:
|
||||
return f"{start.year} 年 {start.month} 月—{end.month} 月"
|
||||
return f"{start.year} 年 {start.month} 月—{end.year} 年 {end.month} 月"
|
||||
if start.year == end.year and start.month == end.month:
|
||||
return f"{start.year} 年 {start.month} 月 {start.day} 日—{end.day} 日"
|
||||
return (
|
||||
f"{start.year} 年 {start.month} 月 {start.day} 日—"
|
||||
f"{end.year} 年 {end.month} 月 {end.day} 日"
|
||||
)
|
||||
|
||||
|
||||
def visible_range_labels(items: Sequence[DateRange]) -> list[str]:
|
||||
"""Use the least date precision that keeps every visible range distinct."""
|
||||
labels = [_range_label(item, "year") for item in items]
|
||||
for precision in ("month", "day"):
|
||||
duplicates = {label for label in labels if labels.count(label) > 1}
|
||||
if not duplicates:
|
||||
break
|
||||
labels = [
|
||||
_range_label(item, precision) if label in duplicates else label
|
||||
for item, label in zip(items, labels, strict=True)
|
||||
]
|
||||
return labels
|
||||
@@ -17,16 +17,14 @@ from datetime import date, datetime, time, timedelta
|
||||
from typing import Final
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
from scripts.dynamic_rectification_copy import (
|
||||
DIMENSION_CONTEXT,
|
||||
SUPPORTED_DIMENSIONS,
|
||||
visible_range_labels,
|
||||
)
|
||||
|
||||
ALGORITHM_VERSION: Final = "birth-time-choice-scoring-v2"
|
||||
MIN_INFORMATION_GAIN: Final = 0.15
|
||||
DIMENSION_CONTEXT: Final = {
|
||||
"education": "一次明显的升学、转学或学习方向变化",
|
||||
"relocation": "一次明显的搬家、离乡或长期居住地变化",
|
||||
"relationship": "一次明显的关系进入、结束或重要转变",
|
||||
"career": "一次明显的工作、职业方向或身份变化",
|
||||
"health_pressure": "一次持续的健康压力或生活压力变化",
|
||||
}
|
||||
SUPPORTED_DIMENSIONS: Final = frozenset(DIMENSION_CONTEXT)
|
||||
|
||||
|
||||
def canonical_hash(value: Mapping | Sequence) -> str:
|
||||
@@ -213,36 +211,6 @@ def opportunities(model: dict) -> list[dict]:
|
||||
return sorted(result, key=lambda item: (-item["estimated_information_gain"], item["opportunity_id"]))
|
||||
|
||||
|
||||
def _range_label(item: dict, precision: str) -> str:
|
||||
start = date.fromisoformat(item["window_start"])
|
||||
end = date.fromisoformat(item["window_end"])
|
||||
if precision == "year":
|
||||
return f"{start.year} 年" if start.year == end.year else f"{start.year}—{end.year} 年"
|
||||
if precision == "month":
|
||||
if start.year == end.year:
|
||||
return f"{start.year} 年 {start.month} 月—{end.month} 月"
|
||||
return f"{start.year} 年 {start.month} 月—{end.year} 年 {end.month} 月"
|
||||
if start.year == end.year and start.month == end.month:
|
||||
return f"{start.year} 年 {start.month} 月 {start.day} 日—{end.day} 日"
|
||||
return (
|
||||
f"{start.year} 年 {start.month} 月 {start.day} 日—"
|
||||
f"{end.year} 年 {end.month} 月 {end.day} 日"
|
||||
)
|
||||
|
||||
|
||||
def _visible_range_labels(items: list[dict]) -> list[str]:
|
||||
labels = [_range_label(item, "year") for item in items]
|
||||
for precision in ("month", "day"):
|
||||
duplicates = {label for label in labels if labels.count(label) > 1}
|
||||
if not duplicates:
|
||||
break
|
||||
labels = [
|
||||
_range_label(item, precision) if label in duplicates else label
|
||||
for item, label in zip(items, labels, strict=True)
|
||||
]
|
||||
return labels
|
||||
|
||||
|
||||
def _dimension_opportunity(dimension: str, windows: list[dict], candidates: list[str]) -> dict | None:
|
||||
neutral_context = DIMENSION_CONTEXT[dimension]
|
||||
memberships: dict[int, list[str]] = defaultdict(list)
|
||||
@@ -269,7 +237,10 @@ def _dimension_opportunity(dimension: str, windows: list[dict], candidates: list
|
||||
}
|
||||
for window, members in populated
|
||||
]
|
||||
labels = _visible_range_labels(basis)
|
||||
labels = visible_range_labels([
|
||||
{"window_start": item["window_start"], "window_end": item["window_end"]}
|
||||
for item in basis
|
||||
])
|
||||
partitions = [
|
||||
{
|
||||
"partition_id": canonical_hash(item),
|
||||
|
||||
@@ -4,8 +4,6 @@ import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts.dynamic_rectification_opportunities import _dimension_opportunity
|
||||
|
||||
TASK2_PACKET_FIXTURE = (
|
||||
@@ -14,20 +12,7 @@ TASK2_PACKET_FIXTURE = (
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("dimension", "domain_terms"),
|
||||
[
|
||||
("education", ("升学", "转学", "学习")),
|
||||
("relocation", ("搬家", "离乡", "居住")),
|
||||
("relationship", ("关系",)),
|
||||
("career", ("工作", "职业", "身份")),
|
||||
("health_pressure", ("健康", "压力", "生活")),
|
||||
],
|
||||
)
|
||||
def test_every_supported_dimension_emits_localized_validator_safe_copy(
|
||||
dimension: str,
|
||||
domain_terms: tuple[str, ...],
|
||||
) -> None:
|
||||
def test_every_supported_dimension_emits_distinct_validator_safe_copy() -> None:
|
||||
windows = [
|
||||
{
|
||||
"window_start": "2018-01-01",
|
||||
@@ -41,26 +26,27 @@ def test_every_supported_dimension_emits_localized_validator_safe_copy(
|
||||
},
|
||||
]
|
||||
|
||||
opportunity = _dimension_opportunity(dimension, windows, ["04:00", "04:01"])
|
||||
|
||||
assert opportunity is not None
|
||||
context = opportunity["neutral_context"]
|
||||
prompt = opportunity["fallback_prompt"]
|
||||
assert any(term in context for term in domain_terms)
|
||||
assert dimension not in context
|
||||
assert re.search(r"[\u3400-\u9fff]", context)
|
||||
assert re.search(r"[A-Za-z]", context) is None
|
||||
assert context in prompt
|
||||
assert prompt.endswith("?")
|
||||
assert re.search(r"[A-Za-z]", prompt) is None
|
||||
assert all(label.endswith(" 年") for label in (
|
||||
item["fallback_label"] for item in opportunity["partitions"]
|
||||
))
|
||||
assert [item["fallback_label"].replace(" 年", "") for item in opportunity["partitions"]] == [
|
||||
"2018—2020",
|
||||
"2021—2023",
|
||||
dimensions = ["education", "relocation", "relationship", "career", "health_pressure"]
|
||||
opportunities = [
|
||||
_dimension_opportunity(dimension, windows, ["04:00", "04:01"])
|
||||
for dimension in dimensions
|
||||
]
|
||||
|
||||
assert all(opportunity is not None for opportunity in opportunities)
|
||||
contexts = [opportunity["neutral_context"] for opportunity in opportunities if opportunity]
|
||||
assert len(contexts) == len(set(contexts)) == len(dimensions)
|
||||
for dimension, opportunity in zip(dimensions, opportunities, strict=True):
|
||||
assert opportunity is not None
|
||||
context = opportunity["neutral_context"]
|
||||
prompt = opportunity["fallback_prompt"]
|
||||
labels = [item["fallback_label"] for item in opportunity["partitions"]]
|
||||
assert dimension not in context
|
||||
assert re.search(r"[\u3400-\u9fff]", context)
|
||||
assert re.search(r"[A-Za-z]", context + prompt) is None
|
||||
assert prompt.endswith("?") and prompt.count("?") == 1
|
||||
assert len(labels) == len(set(label.replace(" ", "") for label in labels))
|
||||
assert all(re.search(r"[\u3400-\u9fff]", label) for label in labels)
|
||||
|
||||
|
||||
def test_frontend_adapter_fixture_is_real_task2_opportunity_output() -> None:
|
||||
packet = json.loads(TASK2_PACKET_FIXTURE.read_text(encoding="utf-8"))
|
||||
@@ -89,11 +75,10 @@ def test_frontend_adapter_fixture_is_real_task2_opportunity_output() -> None:
|
||||
]
|
||||
assert fixture["dimension_code"] == "career"
|
||||
assert "career" not in fixture["neutral_context"]
|
||||
assert fixture["neutral_context"] == opportunity["neutral_context"]
|
||||
assert fixture["fallback_prompt"] == opportunity["fallback_prompt"]
|
||||
assert [item["fallback_label"] for item in fixture["partitions"]] == [
|
||||
item["fallback_label"] for item in opportunity["partitions"]
|
||||
]
|
||||
assert len(fixture["partitions"]) == len(opportunity["partitions"])
|
||||
assert re.search(r"[A-Za-z]", fixture["neutral_context"] + fixture["fallback_prompt"]) is None
|
||||
fixture_labels = [item["fallback_label"] for item in fixture["partitions"]]
|
||||
assert len(fixture_labels) == len(set(label.replace(" ", "") for label in fixture_labels))
|
||||
|
||||
|
||||
def test_same_year_windows_receive_distinct_visible_labels() -> None:
|
||||
@@ -116,3 +101,25 @@ def test_same_year_windows_receive_distinct_visible_labels() -> None:
|
||||
labels = [item["fallback_label"] for item in opportunity["partitions"]]
|
||||
assert len(labels) == len(set(labels))
|
||||
assert all("月" in label for label in labels)
|
||||
|
||||
|
||||
def test_same_month_windows_receive_distinct_day_precision_labels() -> None:
|
||||
windows = [
|
||||
{
|
||||
"window_start": "2012-01-01",
|
||||
"window_end": "2012-01-10",
|
||||
"activations": {"04:00": 1.0, "04:01": 0.0},
|
||||
},
|
||||
{
|
||||
"window_start": "2012-01-11",
|
||||
"window_end": "2012-01-20",
|
||||
"activations": {"04:00": 0.0, "04:01": 1.0},
|
||||
},
|
||||
]
|
||||
|
||||
opportunity = _dimension_opportunity("career", windows, ["04:00", "04:01"])
|
||||
|
||||
assert opportunity is not None
|
||||
labels = [item["fallback_label"] for item in opportunity["partitions"]]
|
||||
assert len(labels) == len(set(labels))
|
||||
assert all("日" in label for label in labels)
|
||||
|
||||
Reference in New Issue
Block a user