Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
342a93caa4 | ||
|
|
0904a4ade7 | ||
|
|
bbd525eeb5 | ||
|
|
352703a91f | ||
|
|
5a439f7ba8 | ||
|
|
f7200b0704 | ||
|
|
14bf8261da | ||
|
|
feb87d5c4e |
@@ -32,7 +32,7 @@ The system already has the correct outbound mutation foundation: Commit owns aut
|
|||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- No Gmail, MCP, Codex Bridge, browser automation, mobile app, voice, or self-extension loop.
|
- No Gmail, MCP, Codex Bridge, mobile app, voice, or self-extension loop. Browser automation is allowed only under ADR-0019 as bounded read-only observation; it is not a human-surface command or mutation channel.
|
||||||
- No new canonical owner.
|
- No new canonical owner.
|
||||||
- No generic scheduler/event-bus/autonomous-agent loop.
|
- No generic scheduler/event-bus/autonomous-agent loop.
|
||||||
- No new third-party dependency.
|
- No new third-party dependency.
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Examples include: a reminder after notification, waiting work requiring a decisi
|
|||||||
|
|
||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- No Codex Bridge, Gmail, MCP, voice, mobile, browser automation, self-extension loop, or generalized orchestration.
|
- No Codex Bridge, Gmail, MCP, voice, mobile, self-extension loop, or generalized orchestration. Browser observation may be used only through the bounded read-only capability defined by ADR-0019 and cannot continue or mutate a response target.
|
||||||
- No universal object ontology.
|
- No universal object ontology.
|
||||||
- No reminder-specific parallel subsystem.
|
- No reminder-specific parallel subsystem.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# ADR-0019 — Bounded Site-Browser Observation
|
||||||
|
|
||||||
|
Status: Accepted
|
||||||
|
|
||||||
|
Task: DEV-000293
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Search snippets are insufficient for dynamic pages such as marketplaces: they may expose a category page while hiding the current product price behind client-side rendering. Domovoy needs a replaceable observation capability that can inspect an allowlisted rendered page without becoming an account agent, universal scraper, or external mutation path.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. Browser automation is allowed only as a server-owned, read-only observation capability behind the existing Capability and research adapter boundaries.
|
||||||
|
2. The browser runtime may navigate only to an explicit HTTPS host allowlist and bounded navigation policy. Browser-supplied or model-supplied authority, credentials, proxy settings, cookies, headers, download paths, or arbitrary JavaScript are rejected.
|
||||||
|
3. The capability may load public pages and wait for bounded client-side rendering. It may read visible text and explicitly declared structured-data selectors, then return an Epistemics Observation with source URL, observed time, coverage, and failure state.
|
||||||
|
4. Login, cookies, account state, CAPTCHA solving, checkout, form submission, uploads, downloads, purchases, messages, mutations, and external side effects are out of scope.
|
||||||
|
5. A blocked page, challenge, timeout, partial DOM, missing price, or conflicting value remains `UNKNOWN`, `PARTIAL`, or `NOT_OBSERVED`; it never becomes a verified fact by model interpretation alone.
|
||||||
|
6. Browser observation is optional. Search-only routing remains the fallback when the capability is unavailable, degraded, or not allowlisted for the requested host.
|
||||||
|
7. Capability owns availability, host policy, health and limits. Epistemics owns the resulting observation and provenance. No new canonical owner or database schema is introduced.
|
||||||
|
8. The first implementation may be development-only. Production activation requires independent capability health, allowlist evidence, adversarial navigation tests, and a live read-only smoke test.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- Core/domain code does not import a browser or provider runtime.
|
||||||
|
- The browser adapter performs only bounded GET/navigation observation and cannot reach Commit/Executor.
|
||||||
|
- Secrets remain outside the browser runtime and its artifacts, logs, screenshots, page content and chat context.
|
||||||
|
- An observation is evidence for answer synthesis, not an instruction to mutate a site or canonical state.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Dynamic public pages can provide stronger evidence than search snippets when the site permits it.
|
||||||
|
- Site-specific selectors and host policy are replaceable adapter configuration, not semantic domain logic.
|
||||||
|
- Browser unavailability is a capability gap, not a runtime-wide outage.
|
||||||
|
- The old blanket prohibition on browser automation is replaced by this narrower, testable boundary.
|
||||||
@@ -572,6 +572,27 @@ func main() {
|
|||||||
semantic.NeedsCognition = false
|
semantic.NeedsCognition = false
|
||||||
semantic.Interpretation.NeedsCognition = false
|
semantic.Interpretation.NeedsCognition = false
|
||||||
}
|
}
|
||||||
|
creativeRequest := interactiondomain.IsCreativeRequest(utterance.Content) || interactiondomain.IsCreativeRequest(effectiveTaskMessage)
|
||||||
|
creativeReply := interactiondomain.CreativeDeterministicReply(utterance.Content)
|
||||||
|
if creativeReply == "" {
|
||||||
|
creativeReply = interactiondomain.CreativeDeterministicReply(effectiveTaskMessage)
|
||||||
|
}
|
||||||
|
creativeFallbackReply := interactiondomain.CreativeFallbackReply(utterance.Content)
|
||||||
|
if creativeFallbackReply == "" {
|
||||||
|
creativeFallbackReply = interactiondomain.CreativeFallbackReply(effectiveTaskMessage)
|
||||||
|
}
|
||||||
|
if creativeRequest {
|
||||||
|
semantic.CapabilityHints = interactiondomain.RemoveCapabilityHint(semantic.CapabilityHints, string(capability.CapabilityKindWebResearch))
|
||||||
|
if creativeReply != "" {
|
||||||
|
semantic.Reply = creativeReply
|
||||||
|
semantic.NeedsCognition = false
|
||||||
|
semantic.Interpretation.NeedsCognition = false
|
||||||
|
} else {
|
||||||
|
semantic.NeedsCognition = true
|
||||||
|
semantic.Interpretation.NeedsCognition = true
|
||||||
|
semantic.CapabilityHints = append(semantic.CapabilityHints, string(capability.CapabilityKindCognitiveReasoning))
|
||||||
|
}
|
||||||
|
}
|
||||||
locationReply := interactiondomain.LocationContextReply(utterance.Content)
|
locationReply := interactiondomain.LocationContextReply(utterance.Content)
|
||||||
if locationReply != "" {
|
if locationReply != "" {
|
||||||
semantic.Reply = locationReply
|
semantic.Reply = locationReply
|
||||||
@@ -778,6 +799,10 @@ func main() {
|
|||||||
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
planningReply = humanCognitiveDegradedReply(utterance.Content)
|
||||||
needsCognition = false
|
needsCognition = false
|
||||||
}
|
}
|
||||||
|
if creativeFallbackReply != "" && cognitiveAttempted && !cognitiveAvailable {
|
||||||
|
planningReply = creativeFallbackReply
|
||||||
|
needsCognition = false
|
||||||
|
}
|
||||||
var capabilityStatus capability.CapabilityResolutionStatus
|
var capabilityStatus capability.CapabilityResolutionStatus
|
||||||
var capabilityKind capability.CapabilityKind
|
var capabilityKind capability.CapabilityKind
|
||||||
var capabilityGapID capability.CapabilityGapID
|
var capabilityGapID capability.CapabilityGapID
|
||||||
@@ -868,7 +893,7 @@ func main() {
|
|||||||
ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
ConfidenceMillis: semantic.Interpretation.ConfidenceMillis,
|
||||||
})
|
})
|
||||||
cancelReview()
|
cancelReview()
|
||||||
stableSemanticReply := identityReply != "" || semantic.InformationRequestKind != "" || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
|
stableSemanticReply := identityReply != "" || creativeReply != "" || (creativeFallbackReply != "" && !cognitiveAvailable) || semantic.InformationRequestKind != "" || locationReply != "" || deterministicSpeechAct == interaction.SpeechActConversational || deterministicSpeechAct == interaction.SpeechActConfirmation || deterministicSpeechAct == interaction.SpeechActRejection
|
||||||
if reviewErr != nil {
|
if reviewErr != nil {
|
||||||
log.Printf("cognitive response review unavailable: %v", reviewErr)
|
log.Printf("cognitive response review unavailable: %v", reviewErr)
|
||||||
} else if !stableSemanticReply && (!researchBacked || reviewPreservesResearchEvidence(reviewedReply, planningReply)) {
|
} else if !stableSemanticReply && (!researchBacked || reviewPreservesResearchEvidence(reviewedReply, planningReply)) {
|
||||||
@@ -890,6 +915,16 @@ func main() {
|
|||||||
needsCognition = false
|
needsCognition = false
|
||||||
planningReply = identityReply
|
planningReply = identityReply
|
||||||
}
|
}
|
||||||
|
if creativeReply != "" {
|
||||||
|
semantic.Reply = creativeReply
|
||||||
|
needsCognition = false
|
||||||
|
planningReply = creativeReply
|
||||||
|
}
|
||||||
|
if creativeFallbackReply != "" && !cognitiveAvailable {
|
||||||
|
semantic.Reply = creativeFallbackReply
|
||||||
|
needsCognition = false
|
||||||
|
planningReply = creativeFallbackReply
|
||||||
|
}
|
||||||
if strings.TrimSpace(planningReply) != "" {
|
if strings.TrimSpace(planningReply) != "" {
|
||||||
semantic.Reply = planningReply
|
semantic.Reply = planningReply
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"task_file": "dev/tasks/DEV-000292-development-mode-runtime-access.json",
|
"task_file": "dev/tasks/DEV-000294-creative-chat-routing.json",
|
||||||
"task_id": "DEV-000292",
|
"task_id": "DEV-000294",
|
||||||
"task_revision": 1
|
"task_revision": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000293",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0019",
|
||||||
|
"base_repo_revision": "origin/main",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Authorize a bounded read-only site-browser capability for dynamic web pages so Domovoy can observe rendered product data, including prices, without granting account access or external mutation authority.",
|
||||||
|
"requirement_ids": ["bounded browser observation", "dynamic web research", "price evidence extraction"],
|
||||||
|
"allowed_change_scope": {
|
||||||
|
"modules": ["capability", "epistemics", "core-runtime", "research"],
|
||||||
|
"paths": [
|
||||||
|
"architecture/adrs/ADR-0014-assistant-presence-attention-human-surface.md",
|
||||||
|
"architecture/adrs/ADR-0015-human-response-continuation-semantics.md",
|
||||||
|
"architecture/adrs/ADR-0019-bounded-site-browser-observation.md",
|
||||||
|
"dev/ACTIVE_TASK.json",
|
||||||
|
"dev/tasks/DEV-000293-site-browser-capability.json",
|
||||||
|
"docs/PROJECT_STATUS.md"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"forbidden_change_scope": {
|
||||||
|
"paths": ["security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||||
|
},
|
||||||
|
"allowed_architecture_delta": {
|
||||||
|
"new_cross_module_dependencies": 0,
|
||||||
|
"new_canonical_owners": 0,
|
||||||
|
"new_privileges": 0,
|
||||||
|
"new_third_party_dependencies": 0,
|
||||||
|
"new_database_owner_roles": 0
|
||||||
|
},
|
||||||
|
"contracts": {
|
||||||
|
"consume": ["existing capability registry", "existing epistemics observation/provenance", "existing research adapter boundary"],
|
||||||
|
"may_change": ["architecture allowance for bounded read-only browser observation"]
|
||||||
|
},
|
||||||
|
"semantic_owner": "capability owns availability and limits; epistemics owns observations and provenance",
|
||||||
|
"canonical_state": ["no new canonical state; browser output remains an observation/artifact projection"],
|
||||||
|
"state_transitions": ["search-only observation -> bounded rendered-page observation -> source-backed answer"],
|
||||||
|
"side_effects_and_privileges": ["read-only HTTPS/browser egress only", "no cookies, account sessions, credentials, downloads, uploads, form submission or external mutation"],
|
||||||
|
"persistence_and_migrations": ["no database migration"],
|
||||||
|
"failure_retry_restart": ["timeouts and blocked pages remain partial/unknown observations", "no blind retries that can duplicate external effects"],
|
||||||
|
"assumptions": ["browser runtime is replaceable and isolated from canonical runtimes", "site allowlist and navigation policy are server-owned"],
|
||||||
|
"acceptance_scenarios": ["dynamic price page can be observed when allowlisted", "401/challenge/timeout is reported as unavailable evidence", "browser cannot submit credentials or mutate a site", "source URL, observed time and coverage remain attached", "existing search-only route remains unchanged when browser capability is unavailable", "production behavior stays read-only"],
|
||||||
|
"required_tests": ["architecture preflight", "check-diff", "ADR consistency", "go test ./...", "go vet ./...", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl independent", "git diff --check"],
|
||||||
|
"terminal_gap_states": ["BLOCKED_BY_ARCHITECTURE_GAP", "BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_CONFLICT", "BLOCKED_BY_MISSING_CAPABILITY"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{
|
||||||
|
"task_id": "DEV-000294",
|
||||||
|
"task_revision": 1,
|
||||||
|
"architecture_revision": "ADR-0019",
|
||||||
|
"base_repo_revision": "origin/main",
|
||||||
|
"risk_tier": "E",
|
||||||
|
"intent": "Route creative requests by meaning: render simple geometric sketches deterministically and send rich creative requests to cognition without web research or truth-evidence fallback.",
|
||||||
|
"requirement_ids": ["creative conversation", "no irrelevant web research", "short deterministic drawing"],
|
||||||
|
"allowed_change_scope": {
|
||||||
|
"modules": ["interaction", "core-runtime"],
|
||||||
|
"paths": [
|
||||||
|
"cmd/core-runtime/**",
|
||||||
|
"modules/interaction/domain/**",
|
||||||
|
"dev/ACTIVE_TASK.json",
|
||||||
|
"dev/tasks/DEV-000294-creative-chat-routing.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"forbidden_change_scope": {
|
||||||
|
"paths": ["architecture/**", "security/**", "database/**", "deploy/docker-compose.prod.yml", "LLM-local/**"]
|
||||||
|
},
|
||||||
|
"allowed_architecture_delta": {
|
||||||
|
"new_cross_module_dependencies": 0,
|
||||||
|
"new_canonical_owners": 0,
|
||||||
|
"new_privileges": 0,
|
||||||
|
"new_third_party_dependencies": 0,
|
||||||
|
"new_database_owner_roles": 0
|
||||||
|
},
|
||||||
|
"contracts": {
|
||||||
|
"consume": ["existing interaction semantic candidate", "existing cognitive gateway", "existing web research route"],
|
||||||
|
"may_change": ["creative capability hints and core route selection"]
|
||||||
|
},
|
||||||
|
"semantic_owner": "interaction owns meaning classification; core-runtime owns route orchestration",
|
||||||
|
"canonical_state": ["existing utterance and candidate history only"],
|
||||||
|
"state_transitions": ["creative request -> deterministic sketch or cognitive conversation route"],
|
||||||
|
"side_effects_and_privileges": ["no new privilege or external mutation"],
|
||||||
|
"persistence_and_migrations": ["no migration"],
|
||||||
|
"failure_retry_restart": ["creative cognition failure reports unavailable cognition; never falls back to web research"],
|
||||||
|
"assumptions": ["simple circle rendering is a text-safe projection; richer images remain a separate capability"],
|
||||||
|
"acceptance_scenarios": ["нарисуй круг returns a circle and no source links", "расскажи сказку о колобке uses cognition and no web route", "ordinary weather and price requests keep web research", "cognitive review cannot replace deterministic circle"],
|
||||||
|
"required_tests": ["go test ./...", "go vet ./...", "go run ./cmd/devctl preflight -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-diff -active dev/ACTIVE_TASK.json", "go run ./cmd/devctl check-architecture", "go run ./cmd/devctl/independent", "git diff --check"],
|
||||||
|
"terminal_gap_states": ["BLOCKED_BY_SPEC_GAP", "BLOCKED_BY_MISSING_CAPABILITY", "BLOCKED_BY_CONFLICT"]
|
||||||
|
}
|
||||||
@@ -495,6 +495,9 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
|||||||
if sourceDependentQuestion(primary, secondary, tokens) {
|
if sourceDependentQuestion(primary, secondary, tokens) {
|
||||||
add("WEB_RESEARCH")
|
add("WEB_RESEARCH")
|
||||||
}
|
}
|
||||||
|
if isCreativeRequestTokens(tokens) {
|
||||||
|
add("COGNITIVE_REASONING")
|
||||||
|
}
|
||||||
if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") {
|
if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") {
|
||||||
add("SCHEDULE_REMINDER")
|
add("SCHEDULE_REMINDER")
|
||||||
}
|
}
|
||||||
@@ -536,6 +539,14 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin
|
|||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isCreativeRequestTokens(tokens []string) bool {
|
||||||
|
if containsStemInTokens(tokens, "нарис", "рису", "изобраз") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return containsStemInTokens(tokens, "расска", "раска", "сочин", "придум") &&
|
||||||
|
containsStemInTokens(tokens, "сказк", "истор", "стих", "шутк", "песн", "басн")
|
||||||
|
}
|
||||||
|
|
||||||
// sourceDependentQuestion identifies semantic classes whose answer depends on
|
// sourceDependentQuestion identifies semantic classes whose answer depends on
|
||||||
// a current or externally observed source. It intentionally uses no named
|
// a current or externally observed source. It intentionally uses no named
|
||||||
// place, person or one-off question branch; when the cue is absent the
|
// place, person or one-off question branch; when the cue is absent the
|
||||||
@@ -639,6 +650,35 @@ func LocationContextReply(message string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsCreativeRequest recognizes generation requests rather than retrieval of
|
||||||
|
// externally verifiable facts. The cognitive route remains responsible for
|
||||||
|
// the requested style and content.
|
||||||
|
func IsCreativeRequest(message string) bool {
|
||||||
|
tokens := semanticTokens(message)
|
||||||
|
return isCreativeRequestTokens(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreativeDeterministicReply covers simple geometric sketches that do not
|
||||||
|
// need a model or external source. Rich creative content remains cognitive.
|
||||||
|
func CreativeDeterministicReply(message string) string {
|
||||||
|
tokens := semanticTokens(message)
|
||||||
|
if containsStemInTokens(tokens, "нарис", "рису", "изобраз") && containsStemInTokens(tokens, "круг", "окруж") {
|
||||||
|
return "◯"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreativeFallbackReply keeps a small creative conversation useful when the
|
||||||
|
// cognitive channel is unavailable. It is not used when cognition returns a
|
||||||
|
// completed richer answer.
|
||||||
|
func CreativeFallbackReply(message string) string {
|
||||||
|
tokens := semanticTokens(message)
|
||||||
|
if IsCreativeRequest(message) && containsStemInTokens(tokens, "сказк", "басн") && containsStemInTokens(tokens, "колоб") {
|
||||||
|
return "Жили-были дед и баба. Испекли они Колобка, а он убежал в лес. Встретил зайца, волка и медведя — от всех укатился, но лису перехитрить не смог. Так Колобок понял: хвастовство и беспечность до добра не доводят."
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 {
|
func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 {
|
||||||
total := 0
|
total := 0
|
||||||
for _, score := range scores {
|
for _, score := range scores {
|
||||||
@@ -818,3 +858,9 @@ func removeCapabilityHint(values []string, denied string) []string {
|
|||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveCapabilityHint lets orchestration boundaries suppress a route that
|
||||||
|
// contradicts an already understood semantic class.
|
||||||
|
func RemoveCapabilityHint(values []string, denied string) []string {
|
||||||
|
return removeCapabilityHint(values, denied)
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package domain
|
package domain
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -215,6 +216,48 @@ func TestMemoryInventoryRequestIsSemantic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestCreativeRequestsDoNotRouteToWebResearch(t *testing.T) {
|
||||||
|
for _, message := range []string{"нарисуй круг", "расскажи сказку о колобке"} {
|
||||||
|
candidate := BuildDeterministicCandidate(message, "", time.Now().UnixMilli())
|
||||||
|
t.Logf("%q candidate=%+v", message, candidate)
|
||||||
|
for _, hint := range candidate.CapabilityHints {
|
||||||
|
if hint == "WEB_RESEARCH" {
|
||||||
|
t.Fatalf("creative request %q unexpectedly selected web research: %+v", message, candidate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInterpretCreativeRequestDoesNotRequireExternalEvidence(t *testing.T) {
|
||||||
|
utterance := interaction.HumanUtterance{Content: "нарисуй круг"}
|
||||||
|
result, err := InterpretUtterance(utterance, nil, nil, time.Now())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Logf("semantic result=%+v", result)
|
||||||
|
if result.Interpretation.SpeechAct == interaction.SpeechActQuestion || containsString(result.CapabilityHints, "WEB_RESEARCH") {
|
||||||
|
t.Fatalf("creative request was routed as external research: %+v", result)
|
||||||
|
}
|
||||||
|
if got := CreativeDeterministicReply("нарисуй круг"); got != "◯" {
|
||||||
|
t.Fatalf("circle reply = %q", got)
|
||||||
|
}
|
||||||
|
if !IsCreativeRequest("расскажи сказку о колобке") {
|
||||||
|
t.Fatal("story request was not recognized as creative")
|
||||||
|
}
|
||||||
|
if got := CreativeFallbackReply("расскажи сказку о колобке"); !strings.Contains(got, "Колоб") {
|
||||||
|
t.Fatalf("story fallback = %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsString(values []string, expected string) bool {
|
||||||
|
for _, value := range values {
|
||||||
|
if value == expected {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func TestLocationContextReply(t *testing.T) {
|
func TestLocationContextReply(t *testing.T) {
|
||||||
if got := LocationContextReply("я сейчас в Москве"); got != "Запомнил: сейчас вы в Москве." {
|
if got := LocationContextReply("я сейчас в Москве"); got != "Запомнил: сейчас вы в Москве." {
|
||||||
t.Fatalf("unexpected location reply %q", got)
|
t.Fatalf("unexpected location reply %q", got)
|
||||||
|
|||||||
Reference in New Issue
Block a user