@kontourai/flow-agents 3.3.0 → 3.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/add-to-project.yml +15 -0
- package/.github/workflows/ci.yml +161 -0
- package/CHANGELOG.md +48 -0
- package/CONTEXT.md +5 -1
- package/README.md +19 -8
- package/build/src/builder-flow-run-adapter.d.ts +80 -0
- package/build/src/builder-flow-run-adapter.js +241 -0
- package/build/src/builder-flow-runtime.d.ts +16 -0
- package/build/src/builder-flow-runtime.js +290 -0
- package/build/src/cli/builder-run.d.ts +1 -0
- package/build/src/cli/builder-run.js +27 -0
- package/build/src/cli/effective-backlog-settings.js +70 -2
- package/build/src/cli/init.d.ts +34 -0
- package/build/src/cli/init.js +341 -61
- package/build/src/cli/kit.js +55 -12
- package/build/src/cli/pull-work-provider.js +346 -5
- package/build/src/cli/skill-drift-check.d.ts +1 -0
- package/build/src/cli/skill-drift-check.js +165 -0
- package/build/src/cli/telemetry-doctor.d.ts +37 -0
- package/build/src/cli/telemetry-doctor.js +53 -6
- package/build/src/cli/validate-hook-influence.js +37 -7
- package/build/src/cli/workflow-sidecar.d.ts +93 -8
- package/build/src/cli/workflow-sidecar.js +1175 -158
- package/build/src/cli.js +5 -0
- package/build/src/flow-kit/validate.d.ts +54 -34
- package/build/src/flow-kit/validate.js +237 -26
- package/build/src/index.d.ts +2 -0
- package/build/src/index.js +1 -0
- package/build/src/lib/console-connect-options.d.ts +97 -0
- package/build/src/lib/console-connect-options.js +199 -0
- package/build/src/lib/console-telemetry-validate.d.ts +49 -0
- package/build/src/lib/console-telemetry-validate.js +91 -0
- package/build/src/lib/flow-resolver.d.ts +56 -3
- package/build/src/lib/flow-resolver.js +151 -11
- package/build/src/lib/fs.d.ts +17 -0
- package/build/src/lib/fs.js +172 -0
- package/build/src/lib/local-artifact-root.d.ts +44 -1
- package/build/src/lib/local-artifact-root.js +131 -3
- package/build/src/runtime-adapters.d.ts +39 -3
- package/build/src/runtime-adapters.js +77 -31
- package/build/src/tools/build-universal-bundles.js +40 -2
- package/build/src/tools/codex-agent-routing.d.ts +2 -0
- package/build/src/tools/codex-agent-routing.js +49 -0
- package/build/src/tools/generate-context-map.js +1 -0
- package/build/src/tools/validate-source-tree.js +27 -1
- package/context/scripts/hooks/lib/kit-catalog.js +235 -0
- package/context/scripts/hooks/lib/runnable-command.js +177 -0
- package/context/scripts/hooks/stop-goal-fit.js +278 -48
- package/context/scripts/hooks/workflow-steering.js +121 -21
- package/context/scripts/package.json +3 -0
- package/context/scripts/telemetry/install-console-config.sh +25 -4
- package/context/scripts/telemetry/lib/config.sh +102 -12
- package/context/scripts/telemetry/lib/pricing.sh +50 -0
- package/context/scripts/telemetry/lib/session.sh +3 -0
- package/context/scripts/telemetry/lib/transport.sh +87 -0
- package/context/scripts/telemetry/lib/usage.sh +205 -4
- package/context/scripts/telemetry/telemetry.conf +6 -0
- package/context/scripts/telemetry/telemetry.sh +48 -0
- package/context/settings/workspace-backlog-provider-settings.example.json +48 -0
- package/docs/agent-usage-feedback-loop.md +35 -0
- package/docs/architecture-engine-and-kits.md +110 -0
- package/docs/context-map.md +2 -0
- package/docs/decisions/embeddable-engine.md +152 -0
- package/docs/decisions/index.md +3 -1
- package/docs/decisions/trust-ledger-retention.md +88 -0
- package/docs/decisions/workflow-enforcement.md +31 -9
- package/docs/fixture-ownership.md +3 -0
- package/docs/implementing-trust-reconciliation.md +129 -0
- package/docs/index.md +19 -9
- package/docs/integrations/flow-agents-console.md +167 -0
- package/docs/kit-authoring-guide.md +52 -21
- package/docs/spec/builder-flow-runtime.md +80 -0
- package/docs/spec/runtime-hook-surface.md +45 -1
- package/docs/specs/economics-record-contract.md +270 -0
- package/docs/specs/harness-capability-matrix.md +74 -0
- package/docs/specs/learning-review-proposals-contract.md +340 -0
- package/docs/specs/routing-efficiency-review.md +59 -0
- package/docs/verifiable-trust.md +74 -25
- package/docs/workflow-usage-guide.md +10 -0
- package/evals/acceptance/prove-capture-teeth.sh +132 -0
- package/evals/ci/antigaming-suite.sh +1 -0
- package/evals/ci/run-baseline.sh +72 -4
- package/evals/fixtures/economics/acceptance.json +12 -0
- package/evals/fixtures/economics/agents/tool-worker-1/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-2/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-3/events.jsonl +2 -0
- package/evals/fixtures/economics/agents/tool-worker-4/events.jsonl +1 -0
- package/evals/fixtures/economics/agents/tool-worker-5/events.jsonl +2 -0
- package/evals/fixtures/economics/critique.json +22 -0
- package/evals/fixtures/economics/expected-record.json +71 -0
- package/evals/fixtures/economics/session-usage-event.json +1 -0
- package/evals/fixtures/economics/state.json +11 -0
- package/evals/fixtures/economics/transcript.jsonl +3 -0
- package/evals/fixtures/hook-influence/cases.json +7 -7
- package/evals/fixtures/learning-review-proposals/balanced/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/economics.jsonl +5 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/effect-follow-up/sessions/task-lr-ef-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.json +30 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/expected-aggregates.md +66 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-1/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-2/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-3/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/gate-review.inquiries.json +26 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-4/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-5/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/pattern-present/sessions/task-lr-pp-6/trust.bundle +21 -0
- package/evals/fixtures/learning-review-proposals/repeat-window/economics.jsonl +6 -0
- package/evals/fixtures/learning-review-proposals/under-threshold/economics.jsonl +3 -0
- package/evals/fixtures/telemetry/usage-transcript-sample.jsonl +4 -0
- package/evals/fixtures/trust-reconcile-exploits/mcp-degrade.json +42 -0
- package/evals/integration/test_builder_entry_enforcement.sh +241 -0
- package/evals/integration/test_builder_step_producers.sh +18 -10
- package/evals/integration/test_bundle_install.sh +172 -0
- package/evals/integration/test_console_tenant_isolation.sh +167 -0
- package/evals/integration/test_critique_supersession_roundtrip.sh +4 -1
- package/evals/integration/test_dual_emit_flow_step.sh +10 -4
- package/evals/integration/test_economics_record.sh +674 -0
- package/evals/integration/test_effective_backlog_settings.sh +1 -1
- package/evals/integration/test_evidence_capture_hook.sh +17 -2
- package/evals/integration/test_exemption_usage_review.sh +198 -0
- package/evals/integration/test_fixture_retirement_audit.sh +2 -2
- package/evals/integration/test_flow_kit_install_git.sh +83 -0
- package/evals/integration/test_flowdef_session_activation.sh +0 -1
- package/evals/integration/test_flowdef_session_history_preservation.sh +13 -3
- package/evals/integration/test_gate_lockdown.sh +7 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +9 -1
- package/evals/integration/test_goal_fit_hook.sh +2031 -0
- package/evals/integration/test_hook_category_behaviors.sh +8 -1
- package/evals/integration/test_hook_influence_cases.sh +25 -1
- package/evals/integration/test_install_merge.sh +227 -2
- package/evals/integration/test_kit_conformance_levels.sh +6 -6
- package/evals/integration/test_learning_review_proposals.sh +329 -0
- package/evals/integration/test_liveness_conflict_injection.sh +26 -22
- package/evals/integration/test_liveness_console_relay.sh +166 -0
- package/evals/integration/test_liveness_heartbeat.sh +17 -17
- package/evals/integration/test_liveness_worktree_root.sh +575 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +6 -1
- package/evals/integration/test_publish_delivery.sh +331 -1
- package/evals/integration/test_pull_work_board.sh +200 -0
- package/evals/integration/test_pull_work_provider.sh +1 -1
- package/evals/integration/test_record_check.sh +378 -0
- package/evals/integration/test_routing_efficiency.sh +71 -0
- package/evals/integration/test_runtime_adapter_activation.sh +28 -0
- package/evals/integration/test_session_resume_roundtrip.sh +16 -19
- package/evals/integration/test_skill_drift_check.sh +870 -0
- package/evals/integration/test_telemetry.sh +445 -0
- package/evals/integration/test_telemetry_doctor.sh +66 -0
- package/evals/integration/test_telemetry_usage_pipeline.sh +228 -0
- package/evals/integration/test_trust_reconcile_negatives.sh +30 -13
- package/evals/integration/test_trust_reconcile_trailer_diagnostic.sh +247 -0
- package/evals/integration/test_usage_cost.sh +61 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +1395 -0
- package/evals/integration/test_workflow_steering_hook.sh +157 -16
- package/evals/integration/test_workspace_settings.sh +176 -0
- package/evals/lib/env.sh +26 -0
- package/evals/lib/node.sh +8 -0
- package/evals/run.sh +29 -0
- package/evals/static/test_ci_integration_coverage.sh +115 -0
- package/evals/static/test_declared_scope_forms_documented.sh +114 -0
- package/evals/static/test_universal_bundles.sh +34 -0
- package/evals/static/test_validate_source_kit_asset_scope.sh +259 -0
- package/evals/static/test_workflow_skills.sh +1 -1
- package/kits/builder/flows/build.flow.json +9 -18
- package/kits/builder/flows/publish-learn.flow.json +5 -1
- package/kits/builder/kit.json +120 -0
- package/kits/builder/skills/deliver/SKILL.md +42 -0
- package/kits/builder/skills/evidence-gate/SKILL.md +12 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -0
- package/kits/builder/skills/learning-review/SKILL.md +51 -0
- package/kits/builder/skills/plan-work/SKILL.md +17 -20
- package/kits/builder/skills/pull-work/SKILL.md +21 -0
- package/kits/builder/skills/release-readiness/SKILL.md +12 -0
- package/kits/knowledge/kit.json +9 -0
- package/kits/veritas-governance/docs/README.md +35 -7
- package/kits/veritas-governance/fixtures/exemption-review/mixed-fresh-stale.DECLARED.json +14 -0
- package/kits/veritas-governance/kit.json +14 -0
- package/kits/veritas-governance/skills/exemption-usage-review/SKILL.md +128 -0
- package/kits/veritas-governance/skills/exemption-usage-review/review-exemptions.mjs +231 -0
- package/package.json +2 -2
- package/packaging/manifest.json +29 -0
- package/schemas/backlog-provider-settings.schema.json +13 -0
- package/schemas/workflow-state.schema.json +44 -0
- package/scripts/README.md +4 -0
- package/scripts/check-content-boundary.cjs +8 -1
- package/scripts/ci/trust-reconcile.js +136 -0
- package/scripts/hooks/codex-hook-adapter.js +77 -2
- package/scripts/hooks/evidence-capture.js +38 -5
- package/scripts/hooks/lib/codex-exit-code.js +316 -0
- package/scripts/hooks/lib/kit-catalog.js +235 -0
- package/scripts/hooks/lib/liveness-write.js +28 -1
- package/scripts/hooks/lib/local-artifact-paths.js +97 -1
- package/scripts/hooks/lib/runnable-command.js +177 -0
- package/scripts/hooks/lib/skill-drift.js +350 -0
- package/scripts/hooks/stop-goal-fit.js +278 -48
- package/scripts/hooks/workflow-steering.js +121 -21
- package/scripts/install-codex-home.sh +97 -47
- package/scripts/install-merge.js +72 -14
- package/scripts/install-owned-files.js +178 -0
- package/scripts/liveness/relay.sh +84 -0
- package/scripts/telemetry/economics-record.schema.json +145 -0
- package/scripts/telemetry/economics-record.sh +331 -0
- package/scripts/telemetry/install-console-config.sh +25 -4
- package/scripts/telemetry/learning-review-decide.sh +124 -0
- package/scripts/telemetry/learning-review-proposals.schema.json +161 -0
- package/scripts/telemetry/learning-review-proposals.sh +484 -0
- package/scripts/telemetry/lib/config.sh +102 -12
- package/scripts/telemetry/lib/pricing.sh +14 -6
- package/scripts/telemetry/lib/session.sh +3 -0
- package/scripts/telemetry/lib/transport.sh +133 -15
- package/scripts/telemetry/lib/usage.sh +121 -28
- package/scripts/telemetry/routing-efficiency.sh +0 -0
- package/scripts/telemetry/telemetry.conf +6 -0
- package/scripts/telemetry/telemetry.sh +48 -0
- package/src/builder-flow-run-adapter.ts +357 -0
- package/src/builder-flow-runtime.ts +348 -0
- package/src/cli/builder-flow-run-adapter.test.mjs +495 -0
- package/src/cli/builder-flow-runtime.test.mjs +213 -0
- package/src/cli/builder-run.ts +28 -0
- package/src/cli/codex-agent-routing.test.mjs +44 -0
- package/src/cli/codex-exit-code.test.mjs +207 -0
- package/src/cli/console-connect-options.test.mjs +329 -0
- package/src/cli/console-telemetry-validate.test.mjs +157 -0
- package/src/cli/effective-backlog-settings.ts +68 -2
- package/src/cli/flow-resolver-composition.test.mjs +101 -0
- package/src/cli/init.test.mjs +161 -0
- package/src/cli/init.ts +407 -62
- package/src/cli/kit-metadata-security.test.mjs +443 -0
- package/src/cli/kit.ts +50 -12
- package/src/cli/pull-work-provider.ts +377 -3
- package/src/cli/sidecar-pure-helpers.test.mjs +64 -0
- package/src/cli/skill-drift-check.ts +196 -0
- package/src/cli/telemetry-doctor.test.mjs +53 -0
- package/src/cli/telemetry-doctor.ts +50 -7
- package/src/cli/validate-hook-influence.ts +37 -6
- package/src/cli/workflow-sidecar.ts +1150 -151
- package/src/cli.ts +5 -0
- package/src/flow-kit/validate.ts +277 -38
- package/src/index.ts +19 -0
- package/src/lib/console-connect-options.ts +261 -0
- package/src/lib/console-telemetry-validate.ts +88 -0
- package/src/lib/flow-resolver.ts +153 -10
- package/src/lib/fs.ts +160 -0
- package/src/lib/local-artifact-root.ts +129 -3
- package/src/runtime-adapters.ts +113 -33
- package/src/tools/build-universal-bundles.ts +36 -2
- package/src/tools/codex-agent-routing.ts +48 -0
- package/src/tools/generate-context-map.ts +1 -0
- package/src/tools/validate-source-tree.ts +26 -1
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_economics_record.sh — per-run kit-economics record (#349, console ADR 0003).
|
|
3
|
+
#
|
|
4
|
+
# Proves scripts/telemetry/economics-record.sh assembles exactly one immutable
|
|
5
|
+
# `kontour.console.economics` v0.1 fact per run — cost, time, iterations, defects caught — from the
|
|
6
|
+
# session.usage event's usage block (TRANSCRIPT ground truth, never re-estimated), the review sidecar
|
|
7
|
+
# critique.json (defects), and state.json (verdict / phase / iterations). And — non-negotiable —
|
|
8
|
+
# local-first holds (ADR 0003 call 6): the record is written to the local economics log FIRST, the
|
|
9
|
+
# console POST is opt-in + detached + fail-open, and a failing/absent console never blocks the run.
|
|
10
|
+
#
|
|
11
|
+
# Coverage → ACs:
|
|
12
|
+
# AC1/AC6 (R1,R7) schema: golden fixture validates; a cost-only doctored record (no defects) FAILS.
|
|
13
|
+
# AC2 (R2,R8) cost.* equals the session.usage usage block; exactly one record; local write.
|
|
14
|
+
# AC3 (R3) phase-sum invariant, phase-known AND phase-unknown (→ unattributed).
|
|
15
|
+
# AC4 (R4,R6) route-back / verdict: iterations.count==2, route_backs==1, verdict PASS, human_wait>0.
|
|
16
|
+
# AC5 (R5) gate_fires, per-severity findings_by_severity, caught_false_completions==1 (distinct).
|
|
17
|
+
# AC7 (R8) telemetry-disabled + console-500 fail-open: run unaffected, local record still exists.
|
|
18
|
+
# injection hostile task_slug/finding text → jq \u-escaped, never raw control bytes in the POST.
|
|
19
|
+
#
|
|
20
|
+
# Deterministic (a local stub HTTP server, no network / model spend, self-cleaning). The relay POST is
|
|
21
|
+
# detached/async so stub-received assertions poll with a timeout.
|
|
22
|
+
# Usage: bash evals/integration/test_economics_record.sh
|
|
23
|
+
set -uo pipefail
|
|
24
|
+
|
|
25
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
26
|
+
TELEMETRY="$ROOT/scripts/telemetry"
|
|
27
|
+
EMITTER="$TELEMETRY/economics-record.sh"
|
|
28
|
+
SCHEMA="$TELEMETRY/economics-record.schema.json"
|
|
29
|
+
FIX="$ROOT/evals/fixtures/economics"
|
|
30
|
+
|
|
31
|
+
TMP="$(mktemp -d)"
|
|
32
|
+
trap 'rm -rf "$TMP"; [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null' EXIT
|
|
33
|
+
PORT=38812
|
|
34
|
+
RECV="$TMP/recv.jsonl"
|
|
35
|
+
|
|
36
|
+
# Hermetic isolation: this suite must be deterministic regardless of what the machine running it has
|
|
37
|
+
# configured for real (e.g. an operator's trusted .kontourai/telemetry-console.conf / ~/.flow-agents
|
|
38
|
+
# conf). Point every emitter invocation below at an empty scratch conf by default so config.sh's
|
|
39
|
+
# key-parser never resolves real console_telemetry_token/console_tenant_id/console_telemetry_url
|
|
40
|
+
# values into these assertions; explicit TELEMETRY_CONFIG_FILE exports in the config-driven (#469)
|
|
41
|
+
# cases below intentionally override this default per-case.
|
|
42
|
+
EMPTY_CONF="$TMP/empty-telemetry.conf"
|
|
43
|
+
: > "$EMPTY_CONF"
|
|
44
|
+
export TELEMETRY_CONFIG_FILE="$EMPTY_CONF"
|
|
45
|
+
|
|
46
|
+
errors=0
|
|
47
|
+
pass() { echo " [PASS] $1"; }
|
|
48
|
+
fail() { echo " [FAIL] $1"; errors=$((errors + 1)); }
|
|
49
|
+
|
|
50
|
+
if ! command -v jq >/dev/null 2>&1; then echo "jq not available; skipping economics record tests"; exit 0; fi
|
|
51
|
+
|
|
52
|
+
echo "=== economics record (#349) ==="
|
|
53
|
+
|
|
54
|
+
# ── Build the session.usage event from the fixture transcript via the REAL usage parser ────────────
|
|
55
|
+
# This is what wires "tokens come from transcript ground truth" into the test: the usage block is
|
|
56
|
+
# produced by usage_parse_transcript reading each assistant message's .message.usage — not hand-set.
|
|
57
|
+
# shellcheck source=/dev/null
|
|
58
|
+
source "$ROOT/scripts/telemetry/lib/usage.sh"
|
|
59
|
+
TU="$(usage_parse_transcript "$FIX/transcript.jsonl")" || { fail "usage_parse_transcript returned empty"; }
|
|
60
|
+
USAGE_EVENT="$(jq -cn --argjson tu "$TU" --arg model "claude-opus-4-8" '{
|
|
61
|
+
schema_version:"0.3.0", timestamp:"1751731200000",
|
|
62
|
+
session_id:"kontourai-flow-agents-349-run-01", event_id:"evt-01-usage",
|
|
63
|
+
event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
|
|
64
|
+
context:{cwd:"/workspace/flow-agents"},
|
|
65
|
+
usage:{ model:$model, duration_s:420, tool_invocations:12, delegations:3,
|
|
66
|
+
input_tokens:$tu.input_tokens, output_tokens:$tu.output_tokens,
|
|
67
|
+
cache_creation_input_tokens:$tu.cache_creation_input_tokens,
|
|
68
|
+
cache_read_input_tokens:$tu.cache_read_input_tokens,
|
|
69
|
+
estimated_cost_usd:$tu.estimated_cost_usd, pricing_version:$tu.pricing_version,
|
|
70
|
+
by_model:$tu.by_model }
|
|
71
|
+
}')"
|
|
72
|
+
|
|
73
|
+
# Run the emitter with an isolated local economics log; echo the assembled record.
|
|
74
|
+
run_emitter() { # <local_log> [extra emitter args...]
|
|
75
|
+
local log="$1"; shift
|
|
76
|
+
TELEMETRY_ECONOMICS_LOG_FILE="$log" bash "$EMITTER" "$USAGE_EVENT" "$@"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# ── AC2 + AC5 + AC4: full fixture (phase-known) → assert every field's source value ────────────────
|
|
80
|
+
echo "--- AC2/AC4/AC5: full fixture record — cost from usage, defects from critique, verdict from state ---"
|
|
81
|
+
LOG1="$TMP/econ1.jsonl"; : > "$LOG1"
|
|
82
|
+
run_emitter "$LOG1" --state "$FIX/state.json" --acceptance "$FIX/acceptance.json" --critique "$FIX/critique.json"
|
|
83
|
+
n=$(wc -l < "$LOG1" | tr -d ' ')
|
|
84
|
+
[[ "$n" == "1" ]] && pass "exactly one record written to the local log (local-first)" || fail "expected 1 record, got $n"
|
|
85
|
+
REC="$(cat "$LOG1")"
|
|
86
|
+
|
|
87
|
+
# cost.* == session.usage .usage.* (ground truth, verbatim)
|
|
88
|
+
assert_eq() { # <label> <jq-expr-on-REC> <expected>
|
|
89
|
+
local got; got="$(printf '%s' "$REC" | jq -c "$2" 2>/dev/null)"
|
|
90
|
+
[[ "$got" == "$3" ]] && pass "$1 ($got)" || fail "$1: expected $3 got $got"
|
|
91
|
+
}
|
|
92
|
+
assert_eq "cost.input_tokens == usage.input_tokens" '.cost.input_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.input_tokens')"
|
|
93
|
+
assert_eq "cost.output_tokens == usage.output_tokens" '.cost.output_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.output_tokens')"
|
|
94
|
+
assert_eq "cost.cache_read_input_tokens == usage" '.cost.cache_read_input_tokens' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.cache_read_input_tokens')"
|
|
95
|
+
assert_eq "cost.estimated_cost_usd == usage" '.cost.estimated_cost_usd' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.estimated_cost_usd')"
|
|
96
|
+
assert_eq "cost.by_model == usage.by_model verbatim" '.cost.by_model' "$(printf '%s' "$USAGE_EVENT" | jq -c '.usage.by_model')"
|
|
97
|
+
assert_eq "model == usage.model" '.model' '"claude-opus-4-8"'
|
|
98
|
+
assert_eq "pricing_version carried" '.pricing_version' '"2026-06-28"'
|
|
99
|
+
assert_eq "run_id == session_id" '.run_id' '"kontourai-flow-agents-349-run-01"'
|
|
100
|
+
assert_eq "task_slug from state.json" '.task_slug' '"kontourai-flow-agents-349"'
|
|
101
|
+
# AC5: defects from critique.json (severity grouping, missing→low; distinct false-completion)
|
|
102
|
+
assert_eq "findings_by_severity.critical" '.defects.findings_by_severity.critical' '1'
|
|
103
|
+
assert_eq "findings_by_severity.high" '.defects.findings_by_severity.high' '2'
|
|
104
|
+
assert_eq "findings_by_severity.medium" '.defects.findings_by_severity.medium' '1'
|
|
105
|
+
assert_eq "findings_by_severity.low (incl. missing-severity default)" '.defects.findings_by_severity.low' '2'
|
|
106
|
+
assert_eq "caught_false_completions == 1 (distinct)" '.defects.caught_false_completions' '1'
|
|
107
|
+
assert_eq "gate_fires from state.json" '.defects.gate_fires' '3'
|
|
108
|
+
# AC4: iterations + verdict + human_wait from state.json
|
|
109
|
+
assert_eq "iterations.count == 2" '.iterations.count' '2'
|
|
110
|
+
assert_eq "iterations.route_backs == 1" '.iterations.route_backs' '1'
|
|
111
|
+
assert_eq "verification_verdict == PASS" '.defects.verification_verdict' '"PASS"'
|
|
112
|
+
assert_eq "human_wait_s > 0 (decision pause present)" '.time.human_wait_s' '45'
|
|
113
|
+
assert_eq "wall_clock_s == usage.duration_s" '.time.wall_clock_s' '420'
|
|
114
|
+
|
|
115
|
+
# ── AC1/AC6: schema validation — golden validates; cost-only (no defects) FAILS (R7 Goodhart) ─────
|
|
116
|
+
echo "--- AC1/AC6: schema — golden validates positive; a cost-only record FAILS (co-required cost+defects) ---"
|
|
117
|
+
SCHEMA_CHECK="$(node -e '
|
|
118
|
+
const Ajv=require("ajv");
|
|
119
|
+
const a=new Ajv({allErrors:true,strict:false});
|
|
120
|
+
const v=a.compile(require(process.argv[1]));
|
|
121
|
+
const golden=require(process.argv[2]);
|
|
122
|
+
const good=v(golden);
|
|
123
|
+
const cur=JSON.parse(require("fs").readFileSync(process.argv[3],"utf8"));
|
|
124
|
+
const curOk=v(cur);
|
|
125
|
+
const bad=JSON.parse(JSON.stringify(golden)); delete bad.defects;
|
|
126
|
+
const badOk=v(bad);
|
|
127
|
+
console.log(JSON.stringify({good, curOk, badOk}));
|
|
128
|
+
' "$SCHEMA" "$FIX/expected-record.json" "$LOG1" 2>&1)"
|
|
129
|
+
echo "$SCHEMA_CHECK" | grep -q '"good":true' && pass "golden fixture validates against economics-record.schema.json" || fail "golden did NOT validate: $SCHEMA_CHECK"
|
|
130
|
+
echo "$SCHEMA_CHECK" | grep -q '"curOk":true' && pass "freshly-emitted record validates against the schema" || fail "emitted record did NOT validate: $SCHEMA_CHECK"
|
|
131
|
+
echo "$SCHEMA_CHECK" | grep -q '"badOk":false' && pass "cost-only record (defects removed) FAILS validation — R7 co-required guard holds" || fail "cost-only record wrongly validated: $SCHEMA_CHECK"
|
|
132
|
+
|
|
133
|
+
# ── golden parity: the emitted record equals the committed golden (sorted) ─────────────────────────
|
|
134
|
+
if diff <(jq -S . "$LOG1") <(jq -S . "$FIX/expected-record.json") >/dev/null 2>&1; then
|
|
135
|
+
pass "emitted record byte-equals the committed golden fixture (expected-record.json)"
|
|
136
|
+
else
|
|
137
|
+
fail "emitted record drifted from golden: $(diff <(jq -S . "$LOG1") <(jq -S . "$FIX/expected-record.json") | head -20)"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
# ── AC3: phase-sum invariant — phase-known AND phase-unknown → unattributed ────────────────────────
|
|
141
|
+
echo "--- AC3: phase-sum invariant holds (phase-known → its bucket; phase-unknown → unattributed) ---"
|
|
142
|
+
check_phase_sum() { # <record> <expected_phase_label>
|
|
143
|
+
local rec="$1" want="$2"
|
|
144
|
+
local ok
|
|
145
|
+
ok="$(printf '%s' "$rec" | jq -c '
|
|
146
|
+
(.phases | map(.input_tokens) | add) as $pi
|
|
147
|
+
| (.phases | map(.output_tokens) | add) as $po
|
|
148
|
+
| (.phases | map(.cache_read_input_tokens) | add) as $pcr
|
|
149
|
+
| (.phases | map(.wall_clock_s) | add) as $pw
|
|
150
|
+
| ($pi == .cost.input_tokens and $po == .cost.output_tokens
|
|
151
|
+
and $pcr == .cost.cache_read_input_tokens and $pw == .time.wall_clock_s)')"
|
|
152
|
+
[[ "$ok" == "true" ]] && pass "phase-sum invariant holds ($want)" || fail "phase-sum invariant BROKEN ($want): $ok"
|
|
153
|
+
local lbl; lbl="$(printf '%s' "$rec" | jq -r '.phases[0].phase')"
|
|
154
|
+
[[ "$lbl" == "$want" ]] && pass "phase label is '$want'" || fail "phase label expected '$want' got '$lbl'"
|
|
155
|
+
}
|
|
156
|
+
check_phase_sum "$REC" "verify"
|
|
157
|
+
# phase-unknown: state.json with NO phase → single unattributed bucket, still sums.
|
|
158
|
+
STATE_NOPHASE="$TMP/state-nophase.json"
|
|
159
|
+
jq 'del(.phase)' "$FIX/state.json" > "$STATE_NOPHASE"
|
|
160
|
+
LOG2="$TMP/econ2.jsonl"; : > "$LOG2"
|
|
161
|
+
run_emitter "$LOG2" --state "$STATE_NOPHASE" --critique "$FIX/critique.json"
|
|
162
|
+
check_phase_sum "$(cat "$LOG2")" "unattributed"
|
|
163
|
+
|
|
164
|
+
# ── injection: hostile task_slug + finding text → jq-escaped, never raw control bytes ─────────────
|
|
165
|
+
echo "--- injection: hostile task_slug/finding text are \\u-escaped, never raw control bytes ---"
|
|
166
|
+
HOSTILE_STATE="$TMP/hostile-state.json"
|
|
167
|
+
HOSTILE_CRIT="$TMP/hostile-crit.json"
|
|
168
|
+
node -e '
|
|
169
|
+
const fs=require("fs");const esc=String.fromCharCode(27),bel=String.fromCharCode(7);
|
|
170
|
+
fs.writeFileSync(process.argv[1], JSON.stringify({schema_version:"1.0",task_slug:"slug"+esc+"[31m"+bel+"EVIL",phase:"exec"+esc+"ute",verification_verdict:"PASS"}));
|
|
171
|
+
fs.writeFileSync(process.argv[2], JSON.stringify({critiques:[{findings:[{severity:"high",description:"finding"+esc+"[2J"+bel+"text"}]}]}));
|
|
172
|
+
' "$HOSTILE_STATE" "$HOSTILE_CRIT"
|
|
173
|
+
LOG3="$TMP/econ3.jsonl"; : > "$LOG3"
|
|
174
|
+
run_emitter "$LOG3" --state "$HOSTILE_STATE" --critique "$HOSTILE_CRIT"
|
|
175
|
+
# Scan for raw control bytes via node (cross-platform; macOS grep lacks -P). ESCAPED ⇒ jq \u-escaped.
|
|
176
|
+
INJ="$(node -e '
|
|
177
|
+
const fs=require("fs");const raw=fs.readFileSync(process.argv[1],"utf8");
|
|
178
|
+
if(!raw.trim()){console.log("EMPTY");process.exit(0)}
|
|
179
|
+
console.log(/[\x00-\x08\x0b\x0c\x0e-\x1f]/.test(raw)?"RAW":"ESCAPED");
|
|
180
|
+
' "$LOG3" 2>&1)"
|
|
181
|
+
[[ "$INJ" == "ESCAPED" ]] && pass "hostile task_slug/finding control bytes are JSON-escaped, never raw in the record (injection discipline)" || fail "raw control bytes reached the record ($INJ)"
|
|
182
|
+
printf '%s' "$(cat "$LOG3")" | jq -e . >/dev/null 2>&1 && pass "record with hostile fields is still valid JSON" || fail "hostile fields produced invalid JSON"
|
|
183
|
+
|
|
184
|
+
# ── Stub Console endpoint (200 / 500) for the relay + fail-open cases ──────────────────────────────
|
|
185
|
+
start_stub() {
|
|
186
|
+
local mode="$1"
|
|
187
|
+
local fake_bin="$TMP/fake-curl-bin"
|
|
188
|
+
mkdir -p "$fake_bin"
|
|
189
|
+
cat > "$fake_bin/curl" <<'SH'
|
|
190
|
+
#!/usr/bin/env bash
|
|
191
|
+
config=""
|
|
192
|
+
while [[ $# -gt 0 ]]; do
|
|
193
|
+
case "$1" in
|
|
194
|
+
--config) config="$2"; shift 2 ;;
|
|
195
|
+
*) shift ;;
|
|
196
|
+
esac
|
|
197
|
+
done
|
|
198
|
+
[[ -n "$config" && -f "$config" ]] || exit 2
|
|
199
|
+
node - "$config" "$ECON_STUB_RECV" <<'NODE'
|
|
200
|
+
const fs = require("fs");
|
|
201
|
+
const config = fs.readFileSync(process.argv[2], "utf8").split(/\n/);
|
|
202
|
+
const out = process.argv[3];
|
|
203
|
+
const headers = {};
|
|
204
|
+
let url = "";
|
|
205
|
+
let method = "GET";
|
|
206
|
+
let body = "";
|
|
207
|
+
for (const line of config) {
|
|
208
|
+
const m = line.match(/^([a-z-]+) = "(.*)"$/);
|
|
209
|
+
if (!m) continue;
|
|
210
|
+
const [, key, value] = m;
|
|
211
|
+
if (key === "url") url = value;
|
|
212
|
+
if (key === "request") method = value;
|
|
213
|
+
if (key === "header") {
|
|
214
|
+
const i = value.indexOf(":");
|
|
215
|
+
if (i > 0) headers[value.slice(0, i).toLowerCase()] = value.slice(i + 1).trim();
|
|
216
|
+
}
|
|
217
|
+
if (key === "data-binary" && value.startsWith("@")) body = fs.readFileSync(value.slice(1), "utf8");
|
|
218
|
+
}
|
|
219
|
+
let path = url;
|
|
220
|
+
try {
|
|
221
|
+
path = new URL(url).pathname;
|
|
222
|
+
} catch {}
|
|
223
|
+
fs.appendFileSync(out, JSON.stringify({ method, url: path, headers, body }) + "\n");
|
|
224
|
+
NODE
|
|
225
|
+
[[ "${ECON_STUB_MODE:-ok}" == "fail" ]] && exit 22
|
|
226
|
+
exit 0
|
|
227
|
+
SH
|
|
228
|
+
chmod +x "$fake_bin/curl"
|
|
229
|
+
export ECON_STUB_RECV="$RECV" ECON_STUB_MODE="$mode"
|
|
230
|
+
case ":$PATH:" in
|
|
231
|
+
*":$fake_bin:"*) ;;
|
|
232
|
+
*) export PATH="$fake_bin:$PATH" ;;
|
|
233
|
+
esac
|
|
234
|
+
STUB_PID=""
|
|
235
|
+
return 0
|
|
236
|
+
}
|
|
237
|
+
stop_stub() { [[ -n "${STUB_PID:-}" ]] && kill "$STUB_PID" 2>/dev/null; STUB_PID=""; }
|
|
238
|
+
wait_for_post() { for _ in $(seq 1 25); do [[ -s "$RECV" ]] && return 0; sleep 0.2; done; return 1; }
|
|
239
|
+
ENDPOINT="http://127.0.0.1:${PORT}/records"
|
|
240
|
+
|
|
241
|
+
# ── local-first: NO console configured → local write happens, nothing POSTed ──────────────────────
|
|
242
|
+
echo "--- local-first: no console configured → local record written, nothing POSTed ---"
|
|
243
|
+
: > "$RECV"; start_stub ok || fail "stub server did not start"
|
|
244
|
+
LOG4="$TMP/econ4.jsonl"; : > "$LOG4"
|
|
245
|
+
(
|
|
246
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
247
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL
|
|
248
|
+
run_emitter "$LOG4" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
249
|
+
)
|
|
250
|
+
sleep 1
|
|
251
|
+
[[ -s "$LOG4" ]] && pass "local economics record written with NO console configured (local-first)" || fail "local record missing with no console"
|
|
252
|
+
[[ ! -s "$RECV" ]] && pass "NO POST when no console endpoint is configured (opt-in respected)" || fail "a POST happened with no console configured"
|
|
253
|
+
stop_stub
|
|
254
|
+
|
|
255
|
+
# ── console configured + relay ON → POST lands with the record + auth headers ─────────────────────
|
|
256
|
+
echo "--- relay on+configured: record mirrors to the Console over the shared transport with auth ---"
|
|
257
|
+
: > "$RECV"; start_stub ok
|
|
258
|
+
LOG5="$TMP/econ5.jsonl"; : > "$LOG5"
|
|
259
|
+
(
|
|
260
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
261
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
262
|
+
export CONSOLE_TELEMETRY_TOKEN="tok-econ-123"
|
|
263
|
+
export CONSOLE_TENANT_ID="tenant-econ"
|
|
264
|
+
run_emitter "$LOG5" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
265
|
+
)
|
|
266
|
+
[[ -s "$LOG5" ]] && pass "local record written even when relay is ON (local-first, still first)" || fail "local record missing with relay on"
|
|
267
|
+
if wait_for_post; then pass "record POSTed to the Console endpoint (opt-in relay)"; else fail "relay did NOT POST within timeout"; fi
|
|
268
|
+
node -e '
|
|
269
|
+
const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
|
|
270
|
+
if(!raw){console.log("NO_POST");process.exit(0)}
|
|
271
|
+
const r=JSON.parse(raw.split("\n")[0]); const b=JSON.parse(r.body);
|
|
272
|
+
const checks=[
|
|
273
|
+
["method POST", r.method==="POST"],
|
|
274
|
+
["authorization bearer", r.headers["authorization"]==="Bearer tok-econ-123"],
|
|
275
|
+
["tenant header", r.headers["x-console-tenant-id"]==="tenant-econ"],
|
|
276
|
+
["schema kontour.console.economics", b.schema==="kontour.console.economics"],
|
|
277
|
+
["version 0.1", b.version==="0.1"],
|
|
278
|
+
["cost.input_tokens 1000", b.cost.input_tokens===1000],
|
|
279
|
+
["defects present (R7 pair shipped)", !!b.defects && typeof b.defects.gate_fires==="number"],
|
|
280
|
+
];
|
|
281
|
+
for(const [n,ok] of checks) console.log((ok?"OK ":"BAD ")+n);
|
|
282
|
+
' "$RECV" > "$TMP/checks.txt" 2>&1
|
|
283
|
+
while IFS= read -r line; do
|
|
284
|
+
[[ "$line" == OK* ]] && pass "POST body ${line#OK }" || fail "POST body ${line#BAD } — $(cat "$TMP/checks.txt")"
|
|
285
|
+
done < <(grep -E '^(OK|BAD) ' "$TMP/checks.txt")
|
|
286
|
+
stop_stub
|
|
287
|
+
|
|
288
|
+
# ── AC7: fail-open — console returns 500 → emitter exits 0, local record intact ───────────────────
|
|
289
|
+
echo "--- AC7: console 500 (detached) → emitter exits 0, local record still exists (fail-open) ---"
|
|
290
|
+
: > "$RECV"; start_stub fail
|
|
291
|
+
LOG6="$TMP/econ6.jsonl"; : > "$LOG6"
|
|
292
|
+
(
|
|
293
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
294
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
295
|
+
run_emitter "$LOG6" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
296
|
+
)
|
|
297
|
+
rc=$?
|
|
298
|
+
[[ "$rc" -eq 0 ]] && pass "emitter exits 0 even when the console endpoint fails (non-fatal)" || fail "emitter exited non-zero ($rc) on console failure"
|
|
299
|
+
[[ -s "$LOG6" ]] && pass "local record written despite console 500 (local-first)" || fail "local record missing on console failure"
|
|
300
|
+
stop_stub
|
|
301
|
+
|
|
302
|
+
# ── AC7: telemetry disabled at the stop-hook → no economics call, run unaffected ──────────────────
|
|
303
|
+
echo "--- AC7: telemetry disabled (TELEMETRY_USAGE_TRACKING!=true) → stop hook emits no economics record ---"
|
|
304
|
+
# The wiring in telemetry.sh guards the economics call behind TELEMETRY_USAGE_TRACKING == true; the
|
|
305
|
+
# emitter itself is unconditional (it is only ever invoked from inside that guard). Assert the guard.
|
|
306
|
+
if grep -q 'economics-record.sh' "$TELEMETRY/telemetry.sh" \
|
|
307
|
+
&& awk '/TELEMETRY_USAGE_TRACKING.*==.*true/{g=1} g&&/economics-record.sh/{found=1} END{exit !found}' "$TELEMETRY/telemetry.sh"; then
|
|
308
|
+
pass "stop-hook economics emission is nested under the TELEMETRY_USAGE_TRACKING==true guard (disabled ⇒ no emit)"
|
|
309
|
+
else
|
|
310
|
+
fail "economics emission is NOT guarded by TELEMETRY_USAGE_TRACKING in telemetry.sh"
|
|
311
|
+
fi
|
|
312
|
+
|
|
313
|
+
# ── config-driven activation (#469): install-console-config.sh sink → relay defaults ON ───────────
|
|
314
|
+
# Mirrors the env-var-driven relay-on/off cases above, but drives FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY /
|
|
315
|
+
# FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL through config.sh's key-parser + install-console-config.sh
|
|
316
|
+
# instead of setting them directly, proving the "no raw env var needed" path (AC2/AC4).
|
|
317
|
+
echo "--- config-driven (#469): install-console-config.sh (no economics flag) → relay defaults ON ---"
|
|
318
|
+
CONF_ON="$TMP/telemetry-cfg-on.conf"
|
|
319
|
+
(
|
|
320
|
+
unset FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL \
|
|
321
|
+
FLOW_AGENTS_CONSOLE_ENDPOINT_URL CONSOLE_TELEMETRY_ENDPOINT_URL \
|
|
322
|
+
FLOW_AGENTS_CONSOLE_TOKEN_FILE CONSOLE_TELEMETRY_TOKEN_FILE \
|
|
323
|
+
FLOW_AGENTS_CONSOLE_TENANT CONSOLE_TENANT_ID
|
|
324
|
+
bash "$TELEMETRY/install-console-config.sh" "$CONF_ON" \
|
|
325
|
+
--console-url "http://127.0.0.1:${PORT}" --console-tenant "tenant-cfg-on" >/dev/null
|
|
326
|
+
)
|
|
327
|
+
RELAY_RESOLVED="$(env -i PATH="$PATH" HOME="$TMP/home-empty" \
|
|
328
|
+
TELEMETRY_CONFIG_FILE="$CONF_ON" TELEMETRY_DATA_DIR="$TMP/cfg-on-data" TELEMETRY_SESSION_DIR="$TMP/cfg-on-data/sessions" \
|
|
329
|
+
bash -c "source '$ROOT/scripts/telemetry/lib/config.sh'; printf '%s' \"\$FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY\"")"
|
|
330
|
+
[[ "$RELAY_RESOLVED" == "1" ]] && pass "config.sh: FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY resolves truthy from a console-sink conf with no explicit key" \
|
|
331
|
+
|| fail "config.sh: expected relay=1, got '$RELAY_RESOLVED'"
|
|
332
|
+
|
|
333
|
+
: > "$RECV"; start_stub ok
|
|
334
|
+
LOG8="$TMP/econ8.jsonl"; : > "$LOG8"
|
|
335
|
+
(
|
|
336
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
337
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
|
|
338
|
+
export TELEMETRY_CONFIG_FILE="$CONF_ON"
|
|
339
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG8"
|
|
340
|
+
bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
341
|
+
)
|
|
342
|
+
[[ -s "$LOG8" ]] && pass "config-driven activation: local record written (local-first)" || fail "config-driven activation: local record missing"
|
|
343
|
+
if wait_for_post; then pass "config-driven activation: emitter POSTs to the derived endpoint with no raw env var set"; else fail "config-driven activation: no POST within timeout"; fi
|
|
344
|
+
stop_stub
|
|
345
|
+
|
|
346
|
+
# ── config-driven opt-out: --no-economics-relay suppresses the POST, local record still holds ─────
|
|
347
|
+
echo "--- config-driven (#469): install-console-config.sh --no-economics-relay suppresses the POST ---"
|
|
348
|
+
CONF_OFF="$TMP/telemetry-cfg-off.conf"
|
|
349
|
+
(
|
|
350
|
+
unset FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL \
|
|
351
|
+
FLOW_AGENTS_CONSOLE_ENDPOINT_URL CONSOLE_TELEMETRY_ENDPOINT_URL \
|
|
352
|
+
FLOW_AGENTS_CONSOLE_TOKEN_FILE CONSOLE_TELEMETRY_TOKEN_FILE \
|
|
353
|
+
FLOW_AGENTS_CONSOLE_TENANT CONSOLE_TENANT_ID
|
|
354
|
+
bash "$TELEMETRY/install-console-config.sh" "$CONF_OFF" \
|
|
355
|
+
--console-url "http://127.0.0.1:${PORT}" --console-tenant "tenant-cfg-off" --no-economics-relay >/dev/null
|
|
356
|
+
)
|
|
357
|
+
grep -q '^console_economics_relay=0$' "$CONF_OFF" && pass "--no-economics-relay writes console_economics_relay=0" || fail "--no-economics-relay did not write console_economics_relay=0"
|
|
358
|
+
|
|
359
|
+
: > "$RECV"; start_stub ok
|
|
360
|
+
LOG9="$TMP/econ9.jsonl"; : > "$LOG9"
|
|
361
|
+
(
|
|
362
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
363
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
|
|
364
|
+
export TELEMETRY_CONFIG_FILE="$CONF_OFF"
|
|
365
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG9"
|
|
366
|
+
bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
367
|
+
)
|
|
368
|
+
sleep 1
|
|
369
|
+
[[ -s "$LOG9" ]] && pass "--no-economics-relay: local record still written (local-first holds)" || fail "--no-economics-relay: local record missing"
|
|
370
|
+
[[ ! -s "$RECV" ]] && pass "--no-economics-relay: relay suppressed even though a console sink is configured" || fail "--no-economics-relay: a POST happened despite opt-out"
|
|
371
|
+
stop_stub
|
|
372
|
+
|
|
373
|
+
# ── explicit console_economics_relay=0 in a hand-written conf suppresses relay too ─────────────────
|
|
374
|
+
echo "--- config-driven (#469): hand-written console_economics_relay=0 suppresses relay despite console URL ---"
|
|
375
|
+
CONF_HAND="$TMP/telemetry-cfg-hand.conf"
|
|
376
|
+
cat > "$CONF_HAND" <<CONF_HAND_EOF
|
|
377
|
+
console_telemetry_url=http://127.0.0.1:${PORT}
|
|
378
|
+
console_tenant_id=tenant-cfg-hand
|
|
379
|
+
console_economics_relay=0
|
|
380
|
+
CONF_HAND_EOF
|
|
381
|
+
|
|
382
|
+
: > "$RECV"; start_stub ok
|
|
383
|
+
LOG10="$TMP/econ10.jsonl"; : > "$LOG10"
|
|
384
|
+
(
|
|
385
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
386
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
|
|
387
|
+
export TELEMETRY_CONFIG_FILE="$CONF_HAND"
|
|
388
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG10"
|
|
389
|
+
bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
390
|
+
)
|
|
391
|
+
sleep 1
|
|
392
|
+
[[ -s "$LOG10" ]] && pass "hand-written console_economics_relay=0: local record still written (local-first holds)" || fail "hand-written conf: local record missing"
|
|
393
|
+
[[ ! -s "$RECV" ]] && pass "hand-written console_economics_relay=0 suppresses relay even with a console URL present" || fail "hand-written conf: a POST happened despite explicit opt-out"
|
|
394
|
+
stop_stub
|
|
395
|
+
|
|
396
|
+
# ── HIGH-finding regression (#469 review): endpoint-only sink (console_telemetry_endpoint_url set,
|
|
397
|
+
# NO console_telemetry_url) must still resolve an economics endpoint — economics-record.sh derives
|
|
398
|
+
# the origin from CONSOLE_TELEMETRY_ENDPOINT_URL (stripping its `/api/telemetry/records` path) and
|
|
399
|
+
# posts to the shared `<origin>/records` ingress, NEVER the telemetry endpoint path verbatim ──────
|
|
400
|
+
echo "--- HIGH regression (#469): endpoint-only sink (console_telemetry_endpoint_url, no console_telemetry_url) still relays economics to <origin>/records ---"
|
|
401
|
+
CONF_ENDPOINT_ONLY="$TMP/telemetry-cfg-endpoint-only.conf"
|
|
402
|
+
cat > "$CONF_ENDPOINT_ONLY" <<CONF_EP_EOF
|
|
403
|
+
console_telemetry_endpoint_url=http://127.0.0.1:${PORT}/api/telemetry/records
|
|
404
|
+
console_tenant_id=tenant-cfg-endpoint-only
|
|
405
|
+
CONF_EP_EOF
|
|
406
|
+
|
|
407
|
+
: > "$RECV"; start_stub ok
|
|
408
|
+
LOG11="$TMP/econ11.jsonl"; : > "$LOG11"
|
|
409
|
+
(
|
|
410
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
411
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
|
|
412
|
+
export TELEMETRY_CONFIG_FILE="$CONF_ENDPOINT_ONLY"
|
|
413
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG11"
|
|
414
|
+
bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
415
|
+
)
|
|
416
|
+
[[ -s "$LOG11" ]] && pass "endpoint-only sink: local record still written (local-first)" || fail "endpoint-only sink: local record missing"
|
|
417
|
+
if wait_for_post; then pass "endpoint-only sink: relay defaults ON (either console signal is truthful, #469 HIGH fix)"; else fail "endpoint-only sink: no POST within timeout (HIGH regression)"; fi
|
|
418
|
+
POSTED_URL="$(node -e '
|
|
419
|
+
const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
|
|
420
|
+
if(!raw){console.log("NO_POST");process.exit(0)}
|
|
421
|
+
const r=JSON.parse(raw.split("\n")[0]);
|
|
422
|
+
console.log(r.url);
|
|
423
|
+
' "$RECV" 2>&1)"
|
|
424
|
+
[[ "$POSTED_URL" == "/records" ]] && pass "endpoint-only sink: POST landed on <origin>/records ($POSTED_URL), NOT the telemetry endpoint path verbatim" \
|
|
425
|
+
|| fail "endpoint-only sink: expected POST to /records, got '$POSTED_URL'"
|
|
426
|
+
stop_stub
|
|
427
|
+
|
|
428
|
+
# ── explicit console_economics_endpoint_url conf key routes the POST to that exact URL ───────────
|
|
429
|
+
echo "--- config-driven (#469): explicit console_economics_endpoint_url conf key wins verbatim ---"
|
|
430
|
+
CONF_EXPLICIT_EP="$TMP/telemetry-cfg-explicit-economics-endpoint.conf"
|
|
431
|
+
cat > "$CONF_EXPLICIT_EP" <<CONF_EXP_EOF
|
|
432
|
+
console_economics_relay=1
|
|
433
|
+
console_economics_endpoint_url=http://127.0.0.1:${PORT}/records
|
|
434
|
+
CONF_EXP_EOF
|
|
435
|
+
|
|
436
|
+
: > "$RECV"; start_stub ok
|
|
437
|
+
LOG12="$TMP/econ12.jsonl"; : > "$LOG12"
|
|
438
|
+
(
|
|
439
|
+
unset FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL \
|
|
440
|
+
FLOW_AGENTS_CONSOLE_URL CONSOLE_TELEMETRY_URL CONSOLE_URL CONSOLE_TENANT_ID CONSOLE_TELEMETRY_TOKEN
|
|
441
|
+
export TELEMETRY_CONFIG_FILE="$CONF_EXPLICIT_EP"
|
|
442
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG12"
|
|
443
|
+
bash "$EMITTER" "$USAGE_EVENT" --state "$FIX/state.json" --critique "$FIX/critique.json"
|
|
444
|
+
)
|
|
445
|
+
[[ -s "$LOG12" ]] && pass "explicit console_economics_endpoint_url: local record still written (local-first)" || fail "explicit console_economics_endpoint_url: local record missing"
|
|
446
|
+
if wait_for_post; then pass "explicit console_economics_endpoint_url: relay POSTs (explicit key + relay=1, no console_telemetry_url needed)"; else fail "explicit console_economics_endpoint_url: no POST within timeout"; fi
|
|
447
|
+
POSTED_URL2="$(node -e '
|
|
448
|
+
const fs=require("fs"); const raw=fs.readFileSync(process.argv[1],"utf8").trim();
|
|
449
|
+
if(!raw){console.log("NO_POST");process.exit(0)}
|
|
450
|
+
const r=JSON.parse(raw.split("\n")[0]);
|
|
451
|
+
console.log(r.url);
|
|
452
|
+
' "$RECV" 2>&1)"
|
|
453
|
+
[[ "$POSTED_URL2" == "/records" ]] && pass "explicit console_economics_endpoint_url: POST landed on the exact configured URL ($POSTED_URL2)" \
|
|
454
|
+
|| fail "explicit console_economics_endpoint_url: expected POST to /records, got '$POSTED_URL2'"
|
|
455
|
+
stop_stub
|
|
456
|
+
|
|
457
|
+
# ── suppression guard: unattributed+$0+no-defect/gate signal never relays (economics-relay- ───────
|
|
458
|
+
# unattributed-suppression) — local write still happens; only the console POST is suppressed. Full
|
|
459
|
+
# signal matrix so a shortcut implementation (task_slug-only) cannot pass: (1) suppressed case, then
|
|
460
|
+
# each signal category alone (attribution / cost / defect-gate) proven to still relay unchanged.
|
|
461
|
+
echo "--- suppression guard: unattributed+\$0+no-defect record is NOT relayed (local write unaffected) ---"
|
|
462
|
+
ZERO_USAGE_EVENT="$(jq -cn '{
|
|
463
|
+
schema_version:"0.3.0", timestamp:"1751731200000",
|
|
464
|
+
session_id:"unattributed-run-01", event_id:"evt-02-usage",
|
|
465
|
+
event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
|
|
466
|
+
context:{cwd:"/workspace/flow-agents"},
|
|
467
|
+
usage:{ model:"claude-opus-4-8", duration_s:5, tool_invocations:0, delegations:0,
|
|
468
|
+
input_tokens:0, output_tokens:0, cache_creation_input_tokens:0, cache_read_input_tokens:0,
|
|
469
|
+
estimated_cost_usd:0, pricing_version:"2026-06-28", by_model:[] }
|
|
470
|
+
}')"
|
|
471
|
+
|
|
472
|
+
# Case 1: no --state/--acceptance/--critique at all (mirrors telemetry.sh's real invocation when
|
|
473
|
+
# econ_slug is empty) + zero-cost usage → local write happens, NO POST.
|
|
474
|
+
: > "$RECV"; start_stub ok || fail "stub server did not start (suppression case 1)"
|
|
475
|
+
LOG13="$TMP/econ13.jsonl"; : > "$LOG13"
|
|
476
|
+
(
|
|
477
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
478
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
479
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG13"
|
|
480
|
+
bash "$EMITTER" "$ZERO_USAGE_EVENT"
|
|
481
|
+
)
|
|
482
|
+
sleep 1
|
|
483
|
+
[[ -s "$LOG13" ]] && pass "suppression case 1: local record still written (local-first unaffected)" || fail "suppression case 1: local record missing"
|
|
484
|
+
[[ ! -s "$RECV" ]] && pass "suppression case 1: unattributed+\$0+no-defect record is NOT POSTed (relay suppressed)" || fail "suppression case 1: a POST happened for an unattributed/\$0/no-defect record"
|
|
485
|
+
stop_stub
|
|
486
|
+
|
|
487
|
+
# Case 1b (HIGH review fix): unattributed + estimated_cost_usd null (unpriced/new model — cost
|
|
488
|
+
# legitimately degrades to null while TOKENS remain source-of-truth, per usage.sh's contract and
|
|
489
|
+
# the #477 fix) + real input/output token volume + no defects → still relays. Without a
|
|
490
|
+
# token-volume leg in has_signal, this record is wrongly suppressed — dropping real ROI data,
|
|
491
|
+
# the opposite of the suppression guard's intent.
|
|
492
|
+
echo "--- suppression guard (HIGH fix): unattributed + null cost + real tokens still relays (no false suppression on unpriced models) ---"
|
|
493
|
+
TOKENS_NULL_COST_USAGE_EVENT="$(jq -cn '{
|
|
494
|
+
schema_version:"0.3.0", timestamp:"1751731200000",
|
|
495
|
+
session_id:"unattributed-unpriced-run-01", event_id:"evt-03-usage",
|
|
496
|
+
event_type:"session.usage", agent:{name:"dev",runtime:"claude-code",version:"1"},
|
|
497
|
+
context:{cwd:"/workspace/flow-agents"},
|
|
498
|
+
usage:{ model:"claude-new-unpriced-model", duration_s:120, tool_invocations:4, delegations:0,
|
|
499
|
+
input_tokens:50000, output_tokens:20000, cache_creation_input_tokens:0, cache_read_input_tokens:0,
|
|
500
|
+
estimated_cost_usd:null, pricing_version:null, by_model:[] }
|
|
501
|
+
}')"
|
|
502
|
+
: > "$RECV"; start_stub ok
|
|
503
|
+
LOG13B="$TMP/econ13b.jsonl"; : > "$LOG13B"
|
|
504
|
+
(
|
|
505
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
506
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
507
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG13B"
|
|
508
|
+
bash "$EMITTER" "$TOKENS_NULL_COST_USAGE_EVENT"
|
|
509
|
+
)
|
|
510
|
+
[[ -s "$LOG13B" ]] && pass "suppression case 1b: local record written" || fail "suppression case 1b: local record missing"
|
|
511
|
+
REC13B="$(cat "$LOG13B")"
|
|
512
|
+
COST13B="$(printf '%s' "$REC13B" | jq -c '.cost.estimated_cost_usd')"
|
|
513
|
+
[[ "$COST13B" == "0" ]] && pass "suppression case 1b: null cost coerces to 0 on the assembled record (estimated_cost_usd==$COST13B)" || fail "suppression case 1b: expected estimated_cost_usd 0, got $COST13B"
|
|
514
|
+
IT13B="$(printf '%s' "$REC13B" | jq -c '.cost.input_tokens')"
|
|
515
|
+
OT13B="$(printf '%s' "$REC13B" | jq -c '.cost.output_tokens')"
|
|
516
|
+
[[ "$IT13B" == "50000" && "$OT13B" == "20000" ]] && pass "suppression case 1b: real token volume carried (input=$IT13B, output=$OT13B)" || fail "suppression case 1b: expected input=50000/output=20000, got input=$IT13B/output=$OT13B"
|
|
517
|
+
if wait_for_post; then pass "suppression case 1b: unattributed + null/\$0 cost + real token volume STILL relays (token-volume leg, no false suppression)"; else fail "suppression case 1b: real-token record was WRONGLY suppressed (token-volume leg missing/broken)"; fi
|
|
518
|
+
stop_stub
|
|
519
|
+
|
|
520
|
+
# Case 2: same zero-cost usage event, but with real task attribution via --state → still relays.
|
|
521
|
+
echo "--- suppression guard: real task-slug attribution alone still relays (zero cost, no defects) ---"
|
|
522
|
+
: > "$RECV"; start_stub ok
|
|
523
|
+
LOG14="$TMP/econ14.jsonl"; : > "$LOG14"
|
|
524
|
+
(
|
|
525
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
526
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
527
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG14"
|
|
528
|
+
bash "$EMITTER" "$ZERO_USAGE_EVENT" --state "$FIX/state.json"
|
|
529
|
+
)
|
|
530
|
+
[[ -s "$LOG14" ]] && pass "suppression case 2: local record written" || fail "suppression case 2: local record missing"
|
|
531
|
+
if wait_for_post; then pass "suppression case 2: real task-slug attribution alone still relays"; else fail "suppression case 2: attributed record was wrongly suppressed"; fi
|
|
532
|
+
stop_stub
|
|
533
|
+
|
|
534
|
+
# Case 3: real-transcript (non-zero cost) usage event, with NO --state/--acceptance/--critique at
|
|
535
|
+
# all (unattributed) → cost signal alone still relays.
|
|
536
|
+
echo "--- suppression guard: real cost alone still relays (unattributed, no defects) ---"
|
|
537
|
+
: > "$RECV"; start_stub ok
|
|
538
|
+
LOG15="$TMP/econ15.jsonl"; : > "$LOG15"
|
|
539
|
+
(
|
|
540
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
541
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
542
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG15"
|
|
543
|
+
bash "$EMITTER" "$USAGE_EVENT"
|
|
544
|
+
)
|
|
545
|
+
[[ -s "$LOG15" ]] && pass "suppression case 3: local record written" || fail "suppression case 3: local record missing"
|
|
546
|
+
if wait_for_post; then pass "suppression case 3: real cost (unattributed) alone still relays"; else fail "suppression case 3: cost-bearing record was wrongly suppressed"; fi
|
|
547
|
+
stop_stub
|
|
548
|
+
|
|
549
|
+
# Case 4: zero-cost usage event, unattributed (task_slug deleted) but a real gate-fire/defect signal
|
|
550
|
+
# via a derived state.json → defect/gate signal alone still relays.
|
|
551
|
+
echo "--- suppression guard: real gate/defect signal alone still relays (unattributed, zero cost) ---"
|
|
552
|
+
STATE_NOSLUG_GATE="$TMP/state-noslug-gate.json"
|
|
553
|
+
jq 'del(.task_slug) | .gate_fires = 2' "$FIX/state.json" > "$STATE_NOSLUG_GATE"
|
|
554
|
+
: > "$RECV"; start_stub ok
|
|
555
|
+
LOG16="$TMP/econ16.jsonl"; : > "$LOG16"
|
|
556
|
+
(
|
|
557
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
558
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
559
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG16"
|
|
560
|
+
bash "$EMITTER" "$ZERO_USAGE_EVENT" --state "$STATE_NOSLUG_GATE"
|
|
561
|
+
)
|
|
562
|
+
[[ -s "$LOG16" ]] && pass "suppression case 4: local record written" || fail "suppression case 4: local record missing"
|
|
563
|
+
if wait_for_post; then pass "suppression case 4: gate/defect signal alone (unattributed, \$0) still relays"; else fail "suppression case 4: gate-signal record was wrongly suppressed"; fi
|
|
564
|
+
stop_stub
|
|
565
|
+
|
|
566
|
+
# Case 5: zero-cost usage event, unattributed (no --state at all), gate_fires=0, findings all 0 —
|
|
567
|
+
# the ONLY signal present is a real caught_false_completions count (set directly on critique.json,
|
|
568
|
+
# not derived from a findings[] entry, so findings_by_severity stays all-zero) → defect sub-leg
|
|
569
|
+
# (caught_false_completions) alone still relays.
|
|
570
|
+
echo "--- suppression guard: caught_false_completions alone still relays (unattributed, \$0, gate_fires=0, findings=0) ---"
|
|
571
|
+
CRITIQUE_CFC_ONLY="$TMP/critique-cfc-only.json"
|
|
572
|
+
printf '%s' '{"gate_fires":0,"caught_false_completions":1}' > "$CRITIQUE_CFC_ONLY"
|
|
573
|
+
: > "$RECV"; start_stub ok
|
|
574
|
+
LOG17="$TMP/econ17.jsonl"; : > "$LOG17"
|
|
575
|
+
(
|
|
576
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
577
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
578
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG17"
|
|
579
|
+
bash "$EMITTER" "$ZERO_USAGE_EVENT" --critique "$CRITIQUE_CFC_ONLY"
|
|
580
|
+
)
|
|
581
|
+
[[ -s "$LOG17" ]] && pass "suppression case 5: local record written" || fail "suppression case 5: local record missing"
|
|
582
|
+
REC17="$(cat "$LOG17")"
|
|
583
|
+
assert_case5() { local got; got="$(printf '%s' "$REC17" | jq -c "$1")"; [[ "$got" == "$2" ]] && pass "suppression case 5: $3 ($got)" || fail "suppression case 5: $3 expected $2 got $got"; }
|
|
584
|
+
assert_case5 '.defects.gate_fires' '0' "gate_fires stays 0 (isolating the cfc sub-leg)"
|
|
585
|
+
assert_case5 '.defects.caught_false_completions' '1' "caught_false_completions carried through"
|
|
586
|
+
assert_case5 '([.defects.findings_by_severity[]] | add)' '0' "findings_by_severity all-zero (isolating the cfc sub-leg)"
|
|
587
|
+
if wait_for_post; then pass "suppression case 5: caught_false_completions signal alone (unattributed, \$0, gate_fires=0) still relays"; else fail "suppression case 5: caught_false_completions-only record was wrongly suppressed"; fi
|
|
588
|
+
stop_stub
|
|
589
|
+
|
|
590
|
+
# Case 6: zero-cost usage event, unattributed (no --state at all), gate_fires=0,
|
|
591
|
+
# caught_false_completions=0 — the ONLY signal present is a nonzero findings_by_severity bucket
|
|
592
|
+
# (one real "high" finding) → defect sub-leg (findings_by_severity) alone still relays.
|
|
593
|
+
echo "--- suppression guard: a nonzero findings_by_severity bucket alone still relays (unattributed, \$0, gate_fires=0, cfc=0) ---"
|
|
594
|
+
CRITIQUE_FINDING_ONLY="$TMP/critique-finding-only.json"
|
|
595
|
+
printf '%s' '{"gate_fires":0,"caught_false_completions":0,"critiques":[{"findings":[{"severity":"high","description":"real finding, not a false-completion"}]}]}' > "$CRITIQUE_FINDING_ONLY"
|
|
596
|
+
: > "$RECV"; start_stub ok
|
|
597
|
+
LOG18="$TMP/econ18.jsonl"; : > "$LOG18"
|
|
598
|
+
(
|
|
599
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_RELAY=1
|
|
600
|
+
export FLOW_AGENTS_CONSOLE_ECONOMICS_ENDPOINT_URL="$ENDPOINT"
|
|
601
|
+
export TELEMETRY_ECONOMICS_LOG_FILE="$LOG18"
|
|
602
|
+
bash "$EMITTER" "$ZERO_USAGE_EVENT" --critique "$CRITIQUE_FINDING_ONLY"
|
|
603
|
+
)
|
|
604
|
+
[[ -s "$LOG18" ]] && pass "suppression case 6: local record written" || fail "suppression case 6: local record missing"
|
|
605
|
+
REC18="$(cat "$LOG18")"
|
|
606
|
+
assert_case6() { local got; got="$(printf '%s' "$REC18" | jq -c "$1")"; [[ "$got" == "$2" ]] && pass "suppression case 6: $3 ($got)" || fail "suppression case 6: $3 expected $2 got $got"; }
|
|
607
|
+
assert_case6 '.defects.gate_fires' '0' "gate_fires stays 0 (isolating the findings sub-leg)"
|
|
608
|
+
assert_case6 '.defects.caught_false_completions' '0' "caught_false_completions stays 0 (isolating the findings sub-leg)"
|
|
609
|
+
assert_case6 '.defects.findings_by_severity.high' '1' "findings_by_severity.high carried through"
|
|
610
|
+
if wait_for_post; then pass "suppression case 6: findings_by_severity signal alone (unattributed, \$0, gate_fires=0, cfc=0) still relays"; else fail "suppression case 6: findings-only record was wrongly suppressed"; fi
|
|
611
|
+
stop_stub
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
# ── AC8 (#415): delegations[] facts + orchestrator-observable outcome + signals capability block ──────
|
|
615
|
+
echo "--- AC8: delegations[] — role/model join, escalation supersession, honest outcome, signals ---"
|
|
616
|
+
LOG7="$TMP/econ7.jsonl"; : > "$LOG7"
|
|
617
|
+
run_emitter "$LOG7" --state "$FIX/state.json" --critique "$FIX/critique.json" --agents-dir "$FIX/agents"
|
|
618
|
+
DREC="$(cat "$LOG7")"
|
|
619
|
+
assert_eq_d() { # <label> <jq-expr-on-DREC> <expected>
|
|
620
|
+
local got; got="$(printf '%s' "$DREC" | jq -c "$2" 2>/dev/null)"
|
|
621
|
+
[[ "$got" == "$3" ]] && pass "$1 ($got)" || fail "$1: expected $3 got $got"
|
|
622
|
+
}
|
|
623
|
+
assert_eq_d "delegations has one entry per delegated agent_id" '.delegations | length' '5'
|
|
624
|
+
assert_eq_d "mechanical delegation carries its role+model" \
|
|
625
|
+
'.delegations[] | select(.agent_id=="tool-worker-1") | [.role,.resolved_model]' \
|
|
626
|
+
'["delegate-mechanical","claude-haiku-4-5@anthropic"]'
|
|
627
|
+
assert_eq_d "escalation supersedes the initial delegation (latest-wins → design/opus)" \
|
|
628
|
+
'.delegations[] | select(.agent_id=="tool-worker-2") | [.role,.resolved_model]' \
|
|
629
|
+
'["delegate-design","claude-opus-4-8@anthropic"]'
|
|
630
|
+
assert_eq_d "escalated_from records the tier it was promoted from" \
|
|
631
|
+
'.delegations[] | select(.agent_id=="tool-worker-2") | .escalated_from' \
|
|
632
|
+
'"delegate-implementation"'
|
|
633
|
+
assert_eq_d "non-escalated delegation has NO escalated_from key" \
|
|
634
|
+
'.delegations[] | select(.agent_id=="tool-worker-1") | has("escalated_from")' 'false'
|
|
635
|
+
# outcome derivation — ONLY from real signals, never fabricated:
|
|
636
|
+
assert_eq_d "outcome accepted (terminal evidence PASS, no escalation/redispatch)" \
|
|
637
|
+
'.delegations[] | select(.agent_id=="tool-worker-1") | .outcome' '"accepted"'
|
|
638
|
+
assert_eq_d "outcome rework (escalation happened)" \
|
|
639
|
+
'.delegations[] | select(.agent_id=="tool-worker-2") | .outcome' '"rework"'
|
|
640
|
+
assert_eq_d "outcome failed (terminal verdict FAIL)" \
|
|
641
|
+
'.delegations[] | select(.agent_id=="tool-worker-3") | .outcome' '"failed"'
|
|
642
|
+
assert_eq_d "outcome UNAVAILABLE when no verdict recorded — NOT assumed accepted" \
|
|
643
|
+
'.delegations[] | select(.agent_id=="tool-worker-4") | .outcome' '"unavailable"'
|
|
644
|
+
assert_eq_d "dispatch_count is orchestrator-observable (single dispatch = 1)" \
|
|
645
|
+
'.delegations[] | select(.agent_id=="tool-worker-1") | .dispatch_count' '1'
|
|
646
|
+
# signals capability block — distinguishes harness-blind from real zero
|
|
647
|
+
assert_eq_d "signals.per_delegation_tokens is false (no runtime isolates sub-agent tokens today)" \
|
|
648
|
+
'.signals.per_delegation_tokens' 'false'
|
|
649
|
+
assert_eq_d "signals.per_delegation_outcome == partial (4 of 5 delegations have a real outcome)" \
|
|
650
|
+
'.signals.per_delegation_outcome' '"partial"'
|
|
651
|
+
assert_eq_d "signals.runtime carried from the usage event" '.signals.runtime' '"claude-code"'
|
|
652
|
+
# re-dispatch WITHOUT escalation is rework via dispatch_count>1 — orchestrator-observable, no sub-agent peek
|
|
653
|
+
assert_eq_d "re-dispatch (2 delegations, no escalation) → dispatch_count=2, outcome rework" \
|
|
654
|
+
'.delegations[] | select(.agent_id=="tool-worker-5") | [.dispatch_count,.outcome]' \
|
|
655
|
+
'[2,"rework"]'
|
|
656
|
+
# absent --agents-dir → delegations [] and signals.per_delegation_outcome n/a (backward compatible)
|
|
657
|
+
assert_eq "delegations defaults to [] with no --agents-dir (golden path)" '.delegations' '[]'
|
|
658
|
+
assert_eq "signals.per_delegation_outcome == n/a with no delegations" '.signals.per_delegation_outcome' '"n/a"'
|
|
659
|
+
# the delegations-bearing record still validates against the schema
|
|
660
|
+
DVAL="$(node -e '
|
|
661
|
+
const Ajv=require("ajv"); const a=new Ajv({allErrors:true,strict:false});
|
|
662
|
+
const v=a.compile(require(process.argv[1]));
|
|
663
|
+
console.log(v(JSON.parse(require("fs").readFileSync(process.argv[2],"utf8")))?"VALID":"INVALID:"+JSON.stringify(v.errors));
|
|
664
|
+
' "$SCHEMA" "$LOG7" 2>&1)"
|
|
665
|
+
[[ "$DVAL" == "VALID" ]] && pass "record with delegations[] + outcome + signals validates against the schema" || fail "delegations record invalid: $DVAL"
|
|
666
|
+
|
|
667
|
+
echo ""
|
|
668
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
669
|
+
echo "test_economics_record: all checks passed."
|
|
670
|
+
exit 0
|
|
671
|
+
else
|
|
672
|
+
echo "test_economics_record: $errors check(s) failed."
|
|
673
|
+
exit 1
|
|
674
|
+
fi
|