@kontourai/flow-agents 2.0.0 → 2.1.0
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/actions/trust-verify/action.yml +4 -2
- package/.github/workflows/ci.yml +12 -0
- package/.github/workflows/runtime-compat.yml +1 -1
- package/CHANGELOG.md +29 -0
- package/README.md +3 -3
- package/build/src/cli/workflow-sidecar.d.ts +16 -0
- package/build/src/cli/workflow-sidecar.js +72 -12
- package/build/src/lib/flow-resolver.d.ts +29 -0
- package/build/src/lib/flow-resolver.js +71 -0
- package/context/scripts/telemetry/lib/config.sh +15 -0
- package/context/scripts/telemetry/telemetry.conf +4 -0
- package/context/scripts/telemetry/telemetry.sh +23 -1
- package/docs/design/flowrun-eventsourcing-design.md +216 -0
- package/docs/design/workflowrun-observability-design.md +431 -0
- package/evals/ci/antigaming-suite.sh +2 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/integration/test_command_log_concurrency.sh +114 -0
- package/evals/integration/test_command_log_fork_classification.sh +134 -0
- package/evals/integration/test_kit_identity_trust.sh +393 -0
- package/evals/integration/test_usage_cost.sh +119 -0
- package/evals/integration/test_verify_cli.sh +23 -0
- package/evals/run.sh +2 -0
- package/integrations/strands/flow_agents_strands/hooks.py +126 -1
- package/integrations/strands/flow_agents_strands/telemetry.py +172 -0
- package/integrations/strands/tests/test_usage.py +129 -0
- package/integrations/strands-ts/src/hooks.ts +135 -1
- package/integrations/strands-ts/src/telemetry.ts +170 -0
- package/integrations/strands-ts/test/test-usage.ts +85 -0
- package/package.json +5 -5
- package/scripts/hooks/evidence-capture.js +75 -13
- package/scripts/hooks/stop-goal-fit.js +76 -23
- package/scripts/repair-command-log.js +115 -0
- package/scripts/telemetry/lib/config.sh +15 -0
- package/scripts/telemetry/lib/pricing.sh +42 -0
- package/scripts/telemetry/lib/usage.sh +108 -0
- package/scripts/telemetry/pricing.golden.json +15 -0
- package/scripts/telemetry/pricing.json +31 -0
- package/scripts/telemetry/telemetry.conf +4 -0
- package/scripts/telemetry/telemetry.sh +23 -1
- package/src/cli/workflow-sidecar.ts +73 -11
- package/src/lib/flow-resolver.ts +85 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_command_log_fork_classification.sh
|
|
3
|
+
#
|
|
4
|
+
# The verifier must tell a BENIGN concurrent fork apart from real TAMPER, and
|
|
5
|
+
# the repair tool must refuse to touch tamper. This is what prevents an honest
|
|
6
|
+
# parallel-write race from becoming a hard block an agent is tempted to launder.
|
|
7
|
+
#
|
|
8
|
+
# forked = two PostToolUse captures share a parent; all hashes self-consistent
|
|
9
|
+
# and reachable. NON-blocking advisory; records stay trusted.
|
|
10
|
+
# broken = content edit (self-hash mismatch) / reorder / deletion / a
|
|
11
|
+
# non-capture sibling on a shared parent. Hard block (unchanged).
|
|
12
|
+
#
|
|
13
|
+
# Also proves: repair re-linearizes forked→ok, and REFUSES broken (no laundering).
|
|
14
|
+
set -uo pipefail
|
|
15
|
+
|
|
16
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
17
|
+
export GATE="$ROOT/scripts/hooks/stop-goal-fit.js"
|
|
18
|
+
REPAIR="$ROOT/scripts/repair-command-log.js"
|
|
19
|
+
|
|
20
|
+
TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT
|
|
21
|
+
errors=0
|
|
22
|
+
_pass() { echo " ✓ $1"; }
|
|
23
|
+
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
24
|
+
|
|
25
|
+
SD=".flow-agents/s"
|
|
26
|
+
|
|
27
|
+
# Build a command-log from a spec: JSON array of {cmd,exit,src,parent} where
|
|
28
|
+
# parent is the 0-based index of the entry whose hash is this entry's prevHash
|
|
29
|
+
# (-1 = genesis). Lets us construct linear chains AND forks deterministically.
|
|
30
|
+
build() { # $1=dir $2=spec-json
|
|
31
|
+
mkdir -p "$1/$SD"
|
|
32
|
+
DIR="$1" node -e '
|
|
33
|
+
const fs=require("fs"),crypto=require("crypto"),path=require("path");
|
|
34
|
+
const g=require(process.env.GATE), GEN=g.CHAIN_GENESIS_VERIFY;
|
|
35
|
+
const canon=r=>{const k=Object.keys(r).filter(x=>x!=="_chain").sort();const o={};for(const x of k)o[x]=r[x];return JSON.stringify(o);};
|
|
36
|
+
const H=(p,r)=>crypto.createHash("sha256").update(p+canon(r)).digest("hex");
|
|
37
|
+
const spec=JSON.parse(process.argv[1]); const hashes=[],lines=[];
|
|
38
|
+
spec.forEach((s,i)=>{
|
|
39
|
+
const rec={command:s.cmd,observedResult:s.exit===0?"pass":"fail",exitCode:s.exit,
|
|
40
|
+
capturedAt:new Date(Date.UTC(2026,0,1,0,0,i)).toISOString(),source:s.src||"postToolUse-capture"};
|
|
41
|
+
const prev=s.parent===-1?GEN:hashes[s.parent]; const h=H(prev,rec);
|
|
42
|
+
hashes.push(h); lines.push(JSON.stringify({...rec,_chain:{seq:i,prevHash:prev,hash:h}}));
|
|
43
|
+
});
|
|
44
|
+
fs.writeFileSync(path.join(process.env.DIR,".flow-agents/s/command-log.jsonl"),lines.join("\n")+"\n");
|
|
45
|
+
' "$2"
|
|
46
|
+
}
|
|
47
|
+
status() { DIR="$1" node -e 'const g=require(process.env.GATE);console.log(g.verifyCommandLogChain(process.env.DIR+"/.flow-agents/s").status)' ; }
|
|
48
|
+
|
|
49
|
+
# ── 1. linear → ok ────────────────────────────────────────────────────────────
|
|
50
|
+
D="$TMP/linear"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0}]'
|
|
51
|
+
[ "$(status "$D")" = "ok" ] && _pass "linear chain → ok" || _fail "linear → $(status "$D"), want ok"
|
|
52
|
+
|
|
53
|
+
# ── 2. concurrent fork (two captures share a parent) → forked ─────────────────
|
|
54
|
+
D="$TMP/fork"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0},{"cmd":"c","exit":0,"parent":0}]'
|
|
55
|
+
[ "$(status "$D")" = "forked" ] && _pass "concurrent fork → forked (not broken)" || _fail "fork → $(status "$D"), want forked"
|
|
56
|
+
|
|
57
|
+
# ── 3. content edit (flip exitCode, keep hash) → broken ───────────────────────
|
|
58
|
+
D="$TMP/flip"; build "$D" '[{"cmd":"npm test","exit":0,"parent":-1},{"cmd":"npm run lint","exit":1,"parent":0}]'
|
|
59
|
+
python3 - "$D/$SD/command-log.jsonl" <<'PY'
|
|
60
|
+
import json,sys
|
|
61
|
+
L=open(sys.argv[1]).read().strip().split("\n"); e=json.loads(L[1]); e["exitCode"]=0; e["observedResult"]="pass"
|
|
62
|
+
L[1]=json.dumps(e); open(sys.argv[1],"w").write("\n".join(L)+"\n")
|
|
63
|
+
PY
|
|
64
|
+
[ "$(status "$D")" = "broken" ] && _pass "content edit → broken (tamper, not fork)" || _fail "flip → $(status "$D"), want broken"
|
|
65
|
+
|
|
66
|
+
# ── 4. reorder → broken ───────────────────────────────────────────────────────
|
|
67
|
+
D="$TMP/reorder"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0}]'
|
|
68
|
+
python3 - "$D/$SD/command-log.jsonl" <<'PY'
|
|
69
|
+
import sys
|
|
70
|
+
L=open(sys.argv[1]).read().strip().split("\n"); L[0],L[1]=L[1],L[0]; open(sys.argv[1],"w").write("\n".join(L)+"\n")
|
|
71
|
+
PY
|
|
72
|
+
[ "$(status "$D")" = "broken" ] && _pass "reorder → broken" || _fail "reorder → $(status "$D"), want broken"
|
|
73
|
+
|
|
74
|
+
# ── 5. deleted predecessor → broken ───────────────────────────────────────────
|
|
75
|
+
D="$TMP/delete"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0}]'
|
|
76
|
+
python3 - "$D/$SD/command-log.jsonl" <<'PY'
|
|
77
|
+
import sys
|
|
78
|
+
L=open(sys.argv[1]).read().strip().split("\n"); open(sys.argv[1],"w").write(L[1]+"\n")
|
|
79
|
+
PY
|
|
80
|
+
[ "$(status "$D")" = "broken" ] && _pass "deleted predecessor → broken" || _fail "delete → $(status "$D"), want broken"
|
|
81
|
+
|
|
82
|
+
# ── 6. non-capture sibling on a shared parent → broken (not a benign fork) ─────
|
|
83
|
+
D="$TMP/badfork"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0},{"cmd":"c","exit":0,"parent":0,"src":"manual-inject"}]'
|
|
84
|
+
[ "$(status "$D")" = "broken" ] && _pass "non-capture sibling fork → broken (conservative)" || _fail "badfork → $(status "$D"), want broken"
|
|
85
|
+
|
|
86
|
+
# ── 7. repair re-linearizes forked → ok; refuses broken ───────────────────────
|
|
87
|
+
D="$TMP/fork2"; build "$D" '[{"cmd":"a","exit":0,"parent":-1},{"cmd":"b","exit":0,"parent":0},{"cmd":"c","exit":0,"parent":0}]'
|
|
88
|
+
node "$REPAIR" "$D/$SD" --reason "test" >/dev/null 2>&1
|
|
89
|
+
[ "$(status "$D")" = "ok" ] && _pass "repair: forked → ok" || _fail "repair forked → $(status "$D"), want ok"
|
|
90
|
+
|
|
91
|
+
D="$TMP/flip2"; build "$D" '[{"cmd":"x","exit":0,"parent":-1},{"cmd":"y","exit":1,"parent":0}]'
|
|
92
|
+
python3 - "$D/$SD/command-log.jsonl" <<'PY'
|
|
93
|
+
import json,sys
|
|
94
|
+
L=open(sys.argv[1]).read().strip().split("\n"); e=json.loads(L[1]); e["exitCode"]=0
|
|
95
|
+
L[1]=json.dumps(e); open(sys.argv[1],"w").write("\n".join(L)+"\n")
|
|
96
|
+
PY
|
|
97
|
+
before=$(cat "$D/$SD/command-log.jsonl")
|
|
98
|
+
set +e; node "$REPAIR" "$D/$SD" >/dev/null 2>&1; rc=$?; set -e
|
|
99
|
+
after=$(cat "$D/$SD/command-log.jsonl")
|
|
100
|
+
if [ "$rc" -ne 0 ] && [ "$before" = "$after" ]; then _pass "repair: REFUSES broken (exit!=0, log unchanged — no laundering)"; else _fail "repair touched/accepted a broken log (rc=$rc)"; fi
|
|
101
|
+
|
|
102
|
+
# ── 8. the Stop gate does NOT hard-block a forked log ─────────────────────────
|
|
103
|
+
D="$TMP/gate"; mkdir -p "$D/$SD"
|
|
104
|
+
printf '# Repo\n' > "$D/AGENTS.md"
|
|
105
|
+
printf '%s' '{"schema_version":"1.0","task_slug":"s","status":"delivered","phase":"done","updated_at":"2026-06-23T00:00:00Z","next_action":{"status":"done","summary":"done"}}' > "$D/$SD/state.json"
|
|
106
|
+
cat > "$D/$SD/s--deliver.md" <<'MD'
|
|
107
|
+
# s
|
|
108
|
+
|
|
109
|
+
branch: main
|
|
110
|
+
status: delivered
|
|
111
|
+
type: deliver
|
|
112
|
+
|
|
113
|
+
## Definition Of Done
|
|
114
|
+
- [x] tests pass
|
|
115
|
+
|
|
116
|
+
## Goal Fit Gate
|
|
117
|
+
- [x] acceptance verified
|
|
118
|
+
|
|
119
|
+
### Verdict: PASS
|
|
120
|
+
MD
|
|
121
|
+
# forked log whose captures are all PASS, so there is no contradiction to flag
|
|
122
|
+
build "$D" '[{"cmd":"npm test","exit":0,"parent":-1},{"cmd":"npm run build","exit":0,"parent":0},{"cmd":"npm run build","exit":0,"parent":0}]'
|
|
123
|
+
printf '%s' '{"schema_version":"1.0","task_slug":"s","verdict":"pass","checks":[{"id":"t","kind":"command","status":"pass","command":"npm test","summary":"ok"}]}' > "$D/$SD/evidence.json"
|
|
124
|
+
set +e
|
|
125
|
+
out=$(FLOW_AGENTS_GOAL_FIT_MODE=block FLOW_AGENTS_GOAL_FIT_BACKSTOP=skip node "$GATE" 2>&1 <<< "{\"hook_event_name\":\"Stop\",\"cwd\":\"$D\"}")
|
|
126
|
+
rc=$?
|
|
127
|
+
set -e
|
|
128
|
+
if [ "$rc" -eq 0 ]; then _pass "gate does NOT hard-block forked log (exit 0)"; else _fail "gate blocked forked log (exit $rc): $out"; fi
|
|
129
|
+
echo "$out" | grep -q "concurrent-capture fork" && _pass "gate emits the concurrent-fork advisory" || _fail "missing fork advisory: $out"
|
|
130
|
+
echo "$out" | grep -q "command-log integrity check FAILED" && _fail "gate wrongly emitted tamper warning for a fork" || _pass "no false tamper warning for a fork"
|
|
131
|
+
|
|
132
|
+
echo ""
|
|
133
|
+
if [ "$errors" -eq 0 ]; then echo "fork classification tests passed."; exit 0; fi
|
|
134
|
+
echo "fork classification tests FAILED: $errors issue(s)."; exit 1
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_kit_identity_trust.sh — Regression eval for kit identity end-to-end in the trust chain.
|
|
3
|
+
#
|
|
4
|
+
# Proves Fix 1 and Fix 2 from the kit-identity task:
|
|
5
|
+
#
|
|
6
|
+
# Fix 1 (surfaceCheckFromArtifact reads kit from bundle, never hardcodes "builder"):
|
|
7
|
+
# 1a. KNOWLEDGE-TYPED bundle → kitIdentityFromBundle derives kitId="knowledge", subject="knowledge-kit"
|
|
8
|
+
# 1b. BUILDER-TYPED bundle → kitIdentityFromBundle derives kitId="builder", subject="builder-kit"
|
|
9
|
+
# 1c. WORKFLOW-ONLY bundle (no kit-typed claim, no current.json) → kitId="unknown", subject="unknown-kit"
|
|
10
|
+
# 1d. record-evidence --surface-trust-json <knowledge-fixture> completes without crash
|
|
11
|
+
#
|
|
12
|
+
# Fix 2 (route-back guard is FlowDefinition-driven, not hardcoded to builder.build):
|
|
13
|
+
# 2a. builder.build: verification→execution still enforced (identical behavior preserved)
|
|
14
|
+
# 2b. Custom non-builder flow WITH route_back_policy: verification→execution ENFORCED
|
|
15
|
+
# 2c. Custom flow WITHOUT route_back_policy: verification→execution NOT ENFORCED
|
|
16
|
+
#
|
|
17
|
+
# Deterministic, no model spend, self-cleaning.
|
|
18
|
+
# Usage: bash evals/integration/test_kit_identity_trust.sh
|
|
19
|
+
|
|
20
|
+
set -uo pipefail
|
|
21
|
+
|
|
22
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
23
|
+
source "$ROOT/evals/lib/node.sh"
|
|
24
|
+
|
|
25
|
+
TMP="$(mktemp -d)"
|
|
26
|
+
errors=0
|
|
27
|
+
|
|
28
|
+
_pass() { echo " ✓ $1"; }
|
|
29
|
+
_fail() { echo " ✗ $1"; errors=$((errors + 1)); }
|
|
30
|
+
|
|
31
|
+
cleanup() { rm -rf "$TMP"; }
|
|
32
|
+
trap cleanup EXIT
|
|
33
|
+
|
|
34
|
+
SIDECAR_JS="${ROOT}/build/src/cli/workflow-sidecar.js"
|
|
35
|
+
SIDECAR_BUNDLE_WRITER="workflow-sidecar"
|
|
36
|
+
|
|
37
|
+
echo ""
|
|
38
|
+
echo "=== Fix 1: kitIdentityFromBundle reads kit from bundle claims (not hardcoded 'builder') ==="
|
|
39
|
+
|
|
40
|
+
# ─── Write fixture bundle files (note: argv[2] = file path since argv[1] = "-" for stdin) ─────────
|
|
41
|
+
|
|
42
|
+
node - "$TMP/knowledge.bundle" << 'NODE'
|
|
43
|
+
const fs = require('fs');
|
|
44
|
+
// argv[0]=node, argv[1]="-", argv[2]=file path
|
|
45
|
+
const bundlePath = process.argv[2];
|
|
46
|
+
const bundle = {
|
|
47
|
+
schemaVersion: 3, source: "test-fixture",
|
|
48
|
+
claims: [{
|
|
49
|
+
id: "c-knowledge-1", claimType: "knowledge.verify.tests",
|
|
50
|
+
subjectType: "flow-step", subjectId: "test-slug/knowledge-ev",
|
|
51
|
+
surface: "flow-agents.workflow", fieldOrBehavior: "knowledge verification",
|
|
52
|
+
value: "pass", status: "verified",
|
|
53
|
+
createdAt: "2026-06-27T00:00:00Z", updatedAt: "2026-06-27T00:00:00Z",
|
|
54
|
+
impactLevel: "high", verificationPolicyId: "policy:knowledge.verify.tests"
|
|
55
|
+
}],
|
|
56
|
+
evidence: [], policies: [], events: []
|
|
57
|
+
};
|
|
58
|
+
fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
59
|
+
NODE
|
|
60
|
+
|
|
61
|
+
node - "$TMP/builder.bundle" << 'NODE'
|
|
62
|
+
const fs = require('fs');
|
|
63
|
+
const bundlePath = process.argv[2];
|
|
64
|
+
const bundle = {
|
|
65
|
+
schemaVersion: 3, source: "test-fixture",
|
|
66
|
+
claims: [{
|
|
67
|
+
id: "c-builder-1", claimType: "builder.verify.tests",
|
|
68
|
+
subjectType: "flow-step", subjectId: "test-slug/builder-ev",
|
|
69
|
+
surface: "flow-agents.workflow", fieldOrBehavior: "builder verification",
|
|
70
|
+
value: "pass", status: "verified",
|
|
71
|
+
createdAt: "2026-06-27T00:00:00Z", updatedAt: "2026-06-27T00:00:00Z",
|
|
72
|
+
impactLevel: "high", verificationPolicyId: "policy:builder.verify.tests"
|
|
73
|
+
}],
|
|
74
|
+
evidence: [], policies: [], events: []
|
|
75
|
+
};
|
|
76
|
+
fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
77
|
+
NODE
|
|
78
|
+
|
|
79
|
+
node - "$TMP/workflow-only.bundle" << 'NODE'
|
|
80
|
+
const fs = require('fs');
|
|
81
|
+
const bundlePath = process.argv[2];
|
|
82
|
+
const bundle = {
|
|
83
|
+
schemaVersion: 3, source: "test-fixture",
|
|
84
|
+
claims: [{
|
|
85
|
+
id: "c-wf-1", claimType: "workflow.check.build",
|
|
86
|
+
subjectType: "workflow-check", subjectId: "test-slug/build",
|
|
87
|
+
surface: "flow-agents.workflow", fieldOrBehavior: "build check",
|
|
88
|
+
value: "pass", status: "verified",
|
|
89
|
+
createdAt: "2026-06-27T00:00:00Z", updatedAt: "2026-06-27T00:00:00Z",
|
|
90
|
+
impactLevel: "high", verificationPolicyId: "policy:workflow.check.build"
|
|
91
|
+
}],
|
|
92
|
+
evidence: [], policies: [], events: []
|
|
93
|
+
};
|
|
94
|
+
fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
95
|
+
NODE
|
|
96
|
+
|
|
97
|
+
echo ""
|
|
98
|
+
echo "=== 1a. KNOWLEDGE-TYPED bundle → kitIdentityFromBundle derives knowledge kit ==="
|
|
99
|
+
KNOWLEDGE_BUNDLE="$TMP/knowledge.bundle"
|
|
100
|
+
SIDECAR_JS_PATH="$SIDECAR_JS"
|
|
101
|
+
node --input-type=module << JSEOF
|
|
102
|
+
import { kitIdentityFromBundle } from '${SIDECAR_JS_PATH}';
|
|
103
|
+
import { readFileSync } from 'node:fs';
|
|
104
|
+
const raw = JSON.parse(readFileSync('${KNOWLEDGE_BUNDLE}', 'utf8'));
|
|
105
|
+
const result = kitIdentityFromBundle(raw, '${KNOWLEDGE_BUNDLE}');
|
|
106
|
+
if (result.kitId !== 'knowledge') throw new Error('Expected kitId=knowledge, got: ' + result.kitId);
|
|
107
|
+
if (result.subject !== 'knowledge-kit') throw new Error('Expected subject=knowledge-kit, got: ' + result.subject);
|
|
108
|
+
if (!result.claimType.startsWith('knowledge.')) throw new Error('Expected claimType to start with knowledge., got: ' + result.claimType);
|
|
109
|
+
if (result.claimType === 'knowledge.trust.bundle') throw new Error('Should use the specific claim type, not the generic fallback, got: ' + result.claimType);
|
|
110
|
+
JSEOF
|
|
111
|
+
if [ $? -eq 0 ]; then
|
|
112
|
+
_pass "KNOWLEDGE bundle: kitId=knowledge, subject=knowledge-kit, claimType=knowledge.verify.tests (not builder)"
|
|
113
|
+
else
|
|
114
|
+
_fail "KNOWLEDGE bundle: expected kitId=knowledge and subject=knowledge-kit, not builder hardcode"
|
|
115
|
+
fi
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "=== 1b. BUILDER-TYPED bundle → kitIdentityFromBundle derives builder kit ==="
|
|
119
|
+
BUILDER_BUNDLE="$TMP/builder.bundle"
|
|
120
|
+
node --input-type=module << JSEOF
|
|
121
|
+
import { kitIdentityFromBundle } from '${SIDECAR_JS_PATH}';
|
|
122
|
+
import { readFileSync } from 'node:fs';
|
|
123
|
+
const raw = JSON.parse(readFileSync('${BUILDER_BUNDLE}', 'utf8'));
|
|
124
|
+
const result = kitIdentityFromBundle(raw, '${BUILDER_BUNDLE}');
|
|
125
|
+
if (result.kitId !== 'builder') throw new Error('Expected kitId=builder, got: ' + result.kitId);
|
|
126
|
+
if (result.subject !== 'builder-kit') throw new Error('Expected subject=builder-kit, got: ' + result.subject);
|
|
127
|
+
if (!result.claimType.startsWith('builder.')) throw new Error('Expected claimType to start with builder., got: ' + result.claimType);
|
|
128
|
+
JSEOF
|
|
129
|
+
if [ $? -eq 0 ]; then
|
|
130
|
+
_pass "BUILDER bundle: kitId=builder, subject=builder-kit (correctly derived from claims, not hardcoded)"
|
|
131
|
+
else
|
|
132
|
+
_fail "BUILDER bundle: expected kitId=builder and subject=builder-kit"
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
echo ""
|
|
136
|
+
echo "=== 1c. WORKFLOW-ONLY bundle (no kit-typed claim, no current.json) → unknown identity ==="
|
|
137
|
+
ISOLATED_DIR="$TMP/isolated-session"
|
|
138
|
+
mkdir -p "$ISOLATED_DIR"
|
|
139
|
+
cp "$TMP/workflow-only.bundle" "$ISOLATED_DIR/workflow-only.bundle"
|
|
140
|
+
WORKFLOW_BUNDLE="$ISOLATED_DIR/workflow-only.bundle"
|
|
141
|
+
node --input-type=module << JSEOF
|
|
142
|
+
import { kitIdentityFromBundle } from '${SIDECAR_JS_PATH}';
|
|
143
|
+
import { readFileSync } from 'node:fs';
|
|
144
|
+
const raw = JSON.parse(readFileSync('${WORKFLOW_BUNDLE}', 'utf8'));
|
|
145
|
+
const result = kitIdentityFromBundle(raw, '${WORKFLOW_BUNDLE}');
|
|
146
|
+
if (result.kitId !== 'unknown') throw new Error('Expected kitId=unknown (no kit-typed claim, no active flow), got: ' + result.kitId);
|
|
147
|
+
if (result.subject !== 'unknown-kit') throw new Error('Expected subject=unknown-kit, got: ' + result.subject);
|
|
148
|
+
if (result.claimType !== 'unknown.trust.bundle') throw new Error('Expected claimType=unknown.trust.bundle, got: ' + result.claimType);
|
|
149
|
+
JSEOF
|
|
150
|
+
if [ $? -eq 0 ]; then
|
|
151
|
+
_pass "WORKFLOW-ONLY bundle: kitId=unknown, subject=unknown-kit (never falls back to builder)"
|
|
152
|
+
else
|
|
153
|
+
_fail "WORKFLOW-ONLY bundle: expected kitId=unknown (no hardcoded builder fallback)"
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
echo ""
|
|
157
|
+
echo "=== 1d. Full pipeline: record-evidence --surface-trust-json with knowledge fixture ==="
|
|
158
|
+
PIPELINE_AROOT="$TMP/pipeline-test/.flow-agents"
|
|
159
|
+
PIPELINE_SLUG="pipeline-kit-identity"
|
|
160
|
+
PIPELINE_DIR="$PIPELINE_AROOT/$PIPELINE_SLUG"
|
|
161
|
+
mkdir -p "$PIPELINE_AROOT"
|
|
162
|
+
|
|
163
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" ensure-session \
|
|
164
|
+
--artifact-root "$PIPELINE_AROOT" \
|
|
165
|
+
--task-slug "$PIPELINE_SLUG" \
|
|
166
|
+
--title "Pipeline kit identity test" \
|
|
167
|
+
--summary "Proves record-evidence processes knowledge bundle without crashing." \
|
|
168
|
+
--criterion "Kit identity preserved" \
|
|
169
|
+
--timestamp "2026-06-27T10:00:00Z" > "$TMP/pipeline-ensure.out" 2>&1
|
|
170
|
+
|
|
171
|
+
KNOWLEDGE_BUNDLE_PATH="$TMP/knowledge.bundle"
|
|
172
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" record-evidence "$PIPELINE_DIR" \
|
|
173
|
+
--verdict not_verified \
|
|
174
|
+
--surface-trust-json "$KNOWLEDGE_BUNDLE_PATH" \
|
|
175
|
+
--timestamp "2026-06-27T10:01:00Z" > "$TMP/pipeline-evidence.out" 2>&1; then
|
|
176
|
+
if [[ -f "$PIPELINE_DIR/trust.bundle" ]]; then
|
|
177
|
+
_pass "record-evidence --surface-trust-json with knowledge bundle completes (pipeline proof: fix is in production code path)"
|
|
178
|
+
else
|
|
179
|
+
_fail "record-evidence --surface-trust-json with knowledge bundle did not write trust.bundle"
|
|
180
|
+
fi
|
|
181
|
+
else
|
|
182
|
+
_fail "record-evidence --surface-trust-json with knowledge bundle failed: $(cat "$TMP/pipeline-evidence.out")"
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
echo ""
|
|
186
|
+
echo "=== Fix 2: FlowDefinition-driven route-back guard ==="
|
|
187
|
+
|
|
188
|
+
# ─── 2a. builder.build: verification→execution still enforced ─────────────────
|
|
189
|
+
echo ""
|
|
190
|
+
echo "=== 2a. builder.build route-back guard: still enforces verification→execution ==="
|
|
191
|
+
BUILDER_DIR="$TMP/fix2-builder/.flow-agents/builder-fix2"
|
|
192
|
+
mkdir -p "$TMP/fix2-builder/.flow-agents"
|
|
193
|
+
|
|
194
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" ensure-session \
|
|
195
|
+
--artifact-root "$TMP/fix2-builder/.flow-agents" \
|
|
196
|
+
--task-slug "builder-fix2" \
|
|
197
|
+
--title "Fix2 builder route-back test" \
|
|
198
|
+
--summary "Verify builder.build route-back still enforced." \
|
|
199
|
+
--timestamp "2026-06-27T10:00:00Z" > "$TMP/fix2-builder-ensure.out" 2>&1
|
|
200
|
+
|
|
201
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$BUILDER_DIR" \
|
|
202
|
+
--status verifying --phase verification \
|
|
203
|
+
--summary "Moving to verification." \
|
|
204
|
+
--flow-definition builder.build \
|
|
205
|
+
--timestamp "2026-06-27T10:01:00Z" > "$TMP/fix2-builder-verify.out" 2>&1
|
|
206
|
+
|
|
207
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$BUILDER_DIR" \
|
|
208
|
+
--status in_progress --phase execution \
|
|
209
|
+
--summary "Route back without reason." \
|
|
210
|
+
--flow-definition builder.build \
|
|
211
|
+
--timestamp "2026-06-27T10:02:00Z" > "$TMP/fix2-builder-noReason.out" 2>&1; then
|
|
212
|
+
_fail "builder.build route-back should require --route-back-reason"
|
|
213
|
+
elif grep -q 'route_back_reason_required' "$TMP/fix2-builder-noReason.out"; then
|
|
214
|
+
_pass "builder.build: verification→execution requires --route-back-reason (identical behavior preserved)"
|
|
215
|
+
else
|
|
216
|
+
_fail "builder.build route-back lacked expected diagnostic (got: $(cat "$TMP/fix2-builder-noReason.out"))"
|
|
217
|
+
fi
|
|
218
|
+
|
|
219
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$BUILDER_DIR" \
|
|
220
|
+
--status in_progress --phase execution \
|
|
221
|
+
--summary "Route back with reason." \
|
|
222
|
+
--flow-definition builder.build \
|
|
223
|
+
--route-back-reason implementation_defect \
|
|
224
|
+
--timestamp "2026-06-27T10:03:00Z" > "$TMP/fix2-builder-withReason.out" 2>&1; then
|
|
225
|
+
_pass "builder.build: verification→execution with reason succeeds (identical behavior preserved)"
|
|
226
|
+
else
|
|
227
|
+
_fail "builder.build route-back with reason should succeed (got: $(cat "$TMP/fix2-builder-withReason.out"))"
|
|
228
|
+
fi
|
|
229
|
+
|
|
230
|
+
# ─── 2b. Custom non-builder flow WITH route_back_policy: enforced ─────────────
|
|
231
|
+
echo ""
|
|
232
|
+
echo "=== 2b. Custom non-builder flow WITH route_back_policy: enforced ==="
|
|
233
|
+
|
|
234
|
+
CUSTOM_FLOWS_DIR="$TMP/custom-flows"
|
|
235
|
+
mkdir -p "$CUSTOM_FLOWS_DIR"
|
|
236
|
+
|
|
237
|
+
# Write acme.deliver flow with route_back_policy (using argv[2] correctly)
|
|
238
|
+
node - "$CUSTOM_FLOWS_DIR/acme.deliver.flow.json" << 'NODE'
|
|
239
|
+
const fs = require('fs');
|
|
240
|
+
const flowPath = process.argv[2];
|
|
241
|
+
const flow = {
|
|
242
|
+
id: "acme.deliver", version: "1.0",
|
|
243
|
+
phase_map: { execution: "execute", verification: "verify" },
|
|
244
|
+
steps: [{ id: "execute", next: "verify" }, { id: "verify", next: "done" }, { id: "done", next: null }],
|
|
245
|
+
gates: {
|
|
246
|
+
"execute-gate": {
|
|
247
|
+
step: "execute",
|
|
248
|
+
expects: [{ id: "execution-scope", kind: "trust.bundle", required: true,
|
|
249
|
+
bundle_claim: { claimType: "acme.execute.scope", subjectType: "change", accepted_statuses: ["trusted","accepted"] } }]
|
|
250
|
+
},
|
|
251
|
+
"verify-gate": {
|
|
252
|
+
step: "verify",
|
|
253
|
+
on_route_back: { implementation_defect: "execute", missing_evidence: "verify", default: "verify" },
|
|
254
|
+
route_back_policy: { max_attempts: 2, on_exceeded: "block" },
|
|
255
|
+
expects: [{ id: "verify-evidence", kind: "trust.bundle", required: true,
|
|
256
|
+
bundle_claim: { claimType: "acme.verify.tests", subjectType: "flow-step", accepted_statuses: ["trusted","accepted"] } }]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
fs.writeFileSync(flowPath, JSON.stringify(flow, null, 2));
|
|
261
|
+
NODE
|
|
262
|
+
|
|
263
|
+
ACME_DIR="$TMP/fix2-acme/.flow-agents/acme-fix2"
|
|
264
|
+
mkdir -p "$TMP/fix2-acme/.flow-agents"
|
|
265
|
+
|
|
266
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" ensure-session \
|
|
267
|
+
--artifact-root "$TMP/fix2-acme/.flow-agents" \
|
|
268
|
+
--task-slug "acme-fix2" \
|
|
269
|
+
--title "Fix2 acme route-back test" \
|
|
270
|
+
--summary "Verify non-builder flow with route_back_policy is enforced." \
|
|
271
|
+
--timestamp "2026-06-27T10:00:00Z" > "$TMP/fix2-acme-ensure.out" 2>&1
|
|
272
|
+
|
|
273
|
+
# Set FLOW_AGENTS_FLOW_DEFS_DIR and export it for the duration of this block
|
|
274
|
+
export FLOW_AGENTS_FLOW_DEFS_DIR="$CUSTOM_FLOWS_DIR"
|
|
275
|
+
|
|
276
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
277
|
+
--status verifying --phase verification \
|
|
278
|
+
--summary "Moving acme to verification." \
|
|
279
|
+
--flow-definition acme.deliver \
|
|
280
|
+
--timestamp "2026-06-27T10:01:00Z" > "$TMP/fix2-acme-verify.out" 2>&1
|
|
281
|
+
|
|
282
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
283
|
+
--status in_progress --phase execution \
|
|
284
|
+
--summary "Acme route back without reason." \
|
|
285
|
+
--flow-definition acme.deliver \
|
|
286
|
+
--timestamp "2026-06-27T10:02:00Z" > "$TMP/fix2-acme-noReason.out" 2>&1; then
|
|
287
|
+
_fail "acme.deliver route-back should require --route-back-reason when route_back_policy is declared"
|
|
288
|
+
elif grep -q 'route_back_reason_required' "$TMP/fix2-acme-noReason.out"; then
|
|
289
|
+
_pass "acme.deliver (non-builder): verification→execution requires reason when route_back_policy declared"
|
|
290
|
+
else
|
|
291
|
+
_fail "acme.deliver route-back lacked expected diagnostic (got: $(cat "$TMP/fix2-acme-noReason.out"))"
|
|
292
|
+
fi
|
|
293
|
+
|
|
294
|
+
# Do 2 successful route-backs
|
|
295
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
296
|
+
--status in_progress --phase execution \
|
|
297
|
+
--summary "Acme route back 1." --flow-definition acme.deliver \
|
|
298
|
+
--route-back-reason implementation_defect \
|
|
299
|
+
--timestamp "2026-06-27T10:03:00Z" > "$TMP/fix2-acme-rb1.out" 2>&1
|
|
300
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
301
|
+
--status verifying --phase verification \
|
|
302
|
+
--summary "Back to verify." --flow-definition acme.deliver \
|
|
303
|
+
--timestamp "2026-06-27T10:04:00Z" > "$TMP/fix2-acme-fwd1.out" 2>&1
|
|
304
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
305
|
+
--status in_progress --phase execution \
|
|
306
|
+
--summary "Acme route back 2." --flow-definition acme.deliver \
|
|
307
|
+
--route-back-reason implementation_defect \
|
|
308
|
+
--timestamp "2026-06-27T10:05:00Z" > "$TMP/fix2-acme-rb2.out" 2>&1
|
|
309
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
310
|
+
--status verifying --phase verification \
|
|
311
|
+
--summary "Back to verify again." --flow-definition acme.deliver \
|
|
312
|
+
--timestamp "2026-06-27T10:06:00Z" > "$TMP/fix2-acme-fwd2.out" 2>&1
|
|
313
|
+
|
|
314
|
+
# Third attempt should exceed max_attempts=2 (flow declares max 2, not hardcoded 3)
|
|
315
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$ACME_DIR" \
|
|
316
|
+
--status in_progress --phase execution \
|
|
317
|
+
--summary "Acme exceeds route-back limit." --flow-definition acme.deliver \
|
|
318
|
+
--route-back-reason implementation_defect \
|
|
319
|
+
--timestamp "2026-06-27T10:07:00Z" > "$TMP/fix2-acme-exceeded.out" 2>&1; then
|
|
320
|
+
_fail "acme.deliver should block after flow-declared max_attempts=2 route-backs"
|
|
321
|
+
elif grep -q 'route_back_attempts_exceeded' "$TMP/fix2-acme-exceeded.out"; then
|
|
322
|
+
_pass "acme.deliver: blocks after flow-declared max_attempts=2 (not the hardcoded 3 from old builder code)"
|
|
323
|
+
else
|
|
324
|
+
_fail "acme.deliver exceeded max_attempts but wrong diagnostic (got: $(cat "$TMP/fix2-acme-exceeded.out"))"
|
|
325
|
+
fi
|
|
326
|
+
|
|
327
|
+
unset FLOW_AGENTS_FLOW_DEFS_DIR
|
|
328
|
+
|
|
329
|
+
# ─── 2c. Custom flow WITHOUT route_back_policy: NOT enforced ──────────────────
|
|
330
|
+
echo ""
|
|
331
|
+
echo "=== 2c. Custom flow WITHOUT route_back_policy: verification→execution NOT enforced ==="
|
|
332
|
+
|
|
333
|
+
CUSTOM_FLOWS_DIR_2="$TMP/custom-flows-2"
|
|
334
|
+
mkdir -p "$CUSTOM_FLOWS_DIR_2"
|
|
335
|
+
|
|
336
|
+
node - "$CUSTOM_FLOWS_DIR_2/acme.nodecl.flow.json" << 'NODE'
|
|
337
|
+
const fs = require('fs');
|
|
338
|
+
const flowPath = process.argv[2];
|
|
339
|
+
const flow = {
|
|
340
|
+
id: "acme.nodecl", version: "1.0",
|
|
341
|
+
phase_map: { execution: "execute", verification: "verify" },
|
|
342
|
+
steps: [{ id: "execute", next: "verify" }, { id: "verify", next: "done" }, { id: "done", next: null }],
|
|
343
|
+
gates: {
|
|
344
|
+
"verify-gate": {
|
|
345
|
+
step: "verify",
|
|
346
|
+
expects: [{ id: "verify-evidence", kind: "trust.bundle", required: true,
|
|
347
|
+
bundle_claim: { claimType: "acme.verify.tests", subjectType: "flow-step", accepted_statuses: ["trusted","accepted"] } }]
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
fs.writeFileSync(flowPath, JSON.stringify(flow, null, 2));
|
|
352
|
+
NODE
|
|
353
|
+
|
|
354
|
+
NODECL_DIR="$TMP/fix2-nodecl/.flow-agents/nodecl-fix2"
|
|
355
|
+
mkdir -p "$TMP/fix2-nodecl/.flow-agents"
|
|
356
|
+
|
|
357
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" ensure-session \
|
|
358
|
+
--artifact-root "$TMP/fix2-nodecl/.flow-agents" \
|
|
359
|
+
--task-slug "nodecl-fix2" \
|
|
360
|
+
--title "Fix2 nodecl route-back test" \
|
|
361
|
+
--summary "Verify flow without route_back_policy is not guarded." \
|
|
362
|
+
--timestamp "2026-06-27T10:00:00Z" > "$TMP/fix2-nodecl-ensure.out" 2>&1
|
|
363
|
+
|
|
364
|
+
export FLOW_AGENTS_FLOW_DEFS_DIR="$CUSTOM_FLOWS_DIR_2"
|
|
365
|
+
|
|
366
|
+
flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$NODECL_DIR" \
|
|
367
|
+
--status verifying --phase verification \
|
|
368
|
+
--summary "Moving nodecl to verification." \
|
|
369
|
+
--flow-definition acme.nodecl \
|
|
370
|
+
--timestamp "2026-06-27T10:01:00Z" > "$TMP/fix2-nodecl-verify.out" 2>&1
|
|
371
|
+
|
|
372
|
+
if flow_agents_node "$SIDECAR_BUNDLE_WRITER" advance-state "$NODECL_DIR" \
|
|
373
|
+
--status in_progress --phase execution \
|
|
374
|
+
--summary "Nodecl route back — should be free without reason." \
|
|
375
|
+
--flow-definition acme.nodecl \
|
|
376
|
+
--timestamp "2026-06-27T10:02:00Z" > "$TMP/fix2-nodecl-rb.out" 2>&1 \
|
|
377
|
+
&& [[ ! -f "$NODECL_DIR/transition-attempts.json" ]]; then
|
|
378
|
+
_pass "acme.nodecl (no route_back_policy): verification→execution freely allowed, no attempts file"
|
|
379
|
+
else
|
|
380
|
+
_fail "acme.nodecl without route_back_policy should allow route-back freely (got: $(cat "$TMP/fix2-nodecl-rb.out"))"
|
|
381
|
+
fi
|
|
382
|
+
|
|
383
|
+
unset FLOW_AGENTS_FLOW_DEFS_DIR
|
|
384
|
+
|
|
385
|
+
echo ""
|
|
386
|
+
echo "────────────────────────────────────────────"
|
|
387
|
+
if [[ "$errors" -eq 0 ]]; then
|
|
388
|
+
echo "test_kit_identity_trust: all checks passed."
|
|
389
|
+
exit 0
|
|
390
|
+
else
|
|
391
|
+
echo "test_kit_identity_trust: $errors check(s) FAILED."
|
|
392
|
+
exit 1
|
|
393
|
+
fi
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_usage_cost.sh — Layer 2 coverage for telemetry usage parsing + cost math.
|
|
3
|
+
#
|
|
4
|
+
# Exercises scripts/telemetry/lib/{pricing,usage}.sh: registry resolution,
|
|
5
|
+
# transcript parsing (per-model tokens + cost), pricing_version stamping,
|
|
6
|
+
# schema-drift detection, version selection, and the cross-runtime golden
|
|
7
|
+
# vectors (scripts/telemetry/pricing.golden.json) that must price identically
|
|
8
|
+
# across bash / Python / the console-telemetry package.
|
|
9
|
+
set -uo pipefail
|
|
10
|
+
|
|
11
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
12
|
+
TELEMETRY="$ROOT/scripts/telemetry"
|
|
13
|
+
GOLDEN="$TELEMETRY/pricing.golden.json"
|
|
14
|
+
|
|
15
|
+
# shellcheck source=/dev/null
|
|
16
|
+
source "$ROOT/scripts/telemetry/lib/usage.sh"
|
|
17
|
+
|
|
18
|
+
pass=0; fail=0
|
|
19
|
+
_pass() { echo " ✓ $1"; pass=$((pass + 1)); }
|
|
20
|
+
_fail() { echo " ✗ $1"; fail=$((fail + 1)); }
|
|
21
|
+
|
|
22
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
23
|
+
echo "jq not available; skipping usage/cost tests"
|
|
24
|
+
exit 0
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
# --- transcript builders ---------------------------------------------------
|
|
28
|
+
mk_line() { # model input output cache_creation cache_read
|
|
29
|
+
jq -nc --arg m "$1" --argjson i "$2" --argjson o "$3" --argjson cc "$4" --argjson cr "$5" \
|
|
30
|
+
'{type:"assistant",message:{model:$m,usage:{input_tokens:$i,output_tokens:$o,cache_creation_input_tokens:$cc,cache_read_input_tokens:$cr}}}'
|
|
31
|
+
}
|
|
32
|
+
approx_eq() { jq -n --argjson a "$1" --argjson e "$2" '((($a)-($e))|if .<0 then -. else . end) < 0.0000005'; }
|
|
33
|
+
|
|
34
|
+
echo "Usage + cost tests"
|
|
35
|
+
|
|
36
|
+
# --- 1. registry resolution -------------------------------------------------
|
|
37
|
+
reg="$(pricing_registry)"
|
|
38
|
+
if echo "$reg" | jq -e '.current_version=="2026-06-28" and (.versions["2026-06-28"].models["claude-opus-4-8"].input==5)' >/dev/null 2>&1; then
|
|
39
|
+
_pass "pricing_registry loads bundled registry (current_version + opus rate)"
|
|
40
|
+
else
|
|
41
|
+
_fail "pricing_registry bundled registry"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
ov="$(mktemp)"; printf '%s' '{"current_version":"ovr","versions":{"ovr":{"cache_multipliers":{"write_5m":1.25,"write_1h":2,"read":0.1},"models":{"claude-opus-4-8":{"input":1,"output":1}},"default":{"input":1,"output":1},"zero_cost_models":["<synthetic>"]}}}' > "$ov"
|
|
45
|
+
if [ "$(TELEMETRY_PRICING_FILE="$ov" pricing_registry | jq -r '.current_version')" = "ovr" ]; then
|
|
46
|
+
_pass "TELEMETRY_PRICING_FILE override wins"
|
|
47
|
+
else
|
|
48
|
+
_fail "TELEMETRY_PRICING_FILE override"
|
|
49
|
+
fi
|
|
50
|
+
|
|
51
|
+
bad="$(mktemp)"; printf 'not json{' > "$bad"
|
|
52
|
+
if [ "$(TELEMETRY_PRICING_FILE="$bad" pricing_registry | jq -r '.current_version' 2>/dev/null)" = "2026-06-28" ]; then
|
|
53
|
+
_fail "malformed override should NOT be used (got bundled — acceptable only if file branch skipped)"
|
|
54
|
+
else
|
|
55
|
+
# malformed file path still exists so it's read raw; pricing_registry cats it. Parser then fails => treated below.
|
|
56
|
+
_pass "malformed override returns raw (parser-level guard covered separately)"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# --- 2. transcript parsing: multi-model ------------------------------------
|
|
60
|
+
tp="$(mktemp)"
|
|
61
|
+
{ mk_line "claude-opus-4-8" 1000 2000 0 500000; mk_line "claude-fable-5" 0 100 0 0; } > "$tp"
|
|
62
|
+
res="$(usage_parse_transcript "$tp")"
|
|
63
|
+
if [ -n "$res" ]; then
|
|
64
|
+
it=$(echo "$res" | jq '.input_tokens'); ot=$(echo "$res" | jq '.output_tokens'); crt=$(echo "$res" | jq '.cache_read_input_tokens')
|
|
65
|
+
pv=$(echo "$res" | jq -r '.pricing_version'); tc=$(echo "$res" | jq '.estimated_cost_usd')
|
|
66
|
+
[ "$it" = "1000" ] && [ "$ot" = "2100" ] && [ "$crt" = "500000" ] && _pass "multi-model token totals" || _fail "multi-model token totals (in=$it out=$ot cr=$crt)"
|
|
67
|
+
[ "$pv" = "2026-06-28" ] && _pass "pricing_version stamped" || _fail "pricing_version stamped (got $pv)"
|
|
68
|
+
# opus 0.305 + fable 0.005 = 0.31
|
|
69
|
+
[ "$(approx_eq "$tc" 0.31)" = "true" ] && _pass "multi-model total cost = 0.31" || _fail "multi-model total cost (got $tc)"
|
|
70
|
+
om=$(echo "$res" | jq '[.by_model[]|select(.model=="claude-opus-4-8")][0].estimated_cost_usd')
|
|
71
|
+
[ "$(approx_eq "$om" 0.305)" = "true" ] && _pass "per-model opus cost = 0.305" || _fail "per-model opus cost (got $om)"
|
|
72
|
+
else
|
|
73
|
+
_fail "multi-model parse returned empty"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# --- 3. empty / no-usage transcript ----------------------------------------
|
|
77
|
+
empty="$(mktemp)"; echo '{"type":"user","message":{"content":"hi"}}' > "$empty"
|
|
78
|
+
if usage_parse_transcript "$empty" >/dev/null 2>&1; then _fail "empty transcript should return non-zero"; else _pass "empty transcript → non-zero (null fallback)"; fi
|
|
79
|
+
|
|
80
|
+
# --- 4. schema drift: usage present under unexpected path -------------------
|
|
81
|
+
drift="$(mktemp)"; echo '{"type":"assistant","message_v2":{"usage":{"input_tokens":999}}}' > "$drift"
|
|
82
|
+
dlog="$(mktemp)"
|
|
83
|
+
if TELEMETRY_DRIFT_LOG="$dlog" usage_parse_transcript "$drift" >/dev/null 2>&1; then
|
|
84
|
+
_fail "drift transcript should return non-zero"
|
|
85
|
+
else
|
|
86
|
+
if grep -q "drift" "$dlog" 2>/dev/null; then _pass "schema drift detected + logged"; else _fail "drift not logged"; fi
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# --- 5. version selection ---------------------------------------------------
|
|
90
|
+
tp2="$(mktemp)"; mk_line "claude-opus-4-8" 0 1000000 0 0 > "$tp2"
|
|
91
|
+
v2="$(mktemp)"; printf '%s' '{"current_version":"new","versions":{"new":{"cache_multipliers":{"write_5m":1.25,"write_1h":2,"read":0.1},"models":{"claude-opus-4-8":{"input":5,"output":25}},"default":{"input":5,"output":25},"zero_cost_models":[]},"old":{"cache_multipliers":{"write_5m":1.25,"write_1h":2,"read":0.1},"models":{"claude-opus-4-8":{"input":1,"output":1}},"default":{"input":1,"output":1},"zero_cost_models":[]}}}' > "$v2"
|
|
92
|
+
new_cost=$(TELEMETRY_PRICING_FILE="$v2" usage_parse_transcript "$tp2" | jq '.estimated_cost_usd')
|
|
93
|
+
old_cost=$(TELEMETRY_PRICING_FILE="$v2" usage_parse_transcript "$tp2" "old" | jq '.estimated_cost_usd')
|
|
94
|
+
{ [ "$(approx_eq "$new_cost" 25)" = "true" ] && [ "$(approx_eq "$old_cost" 1)" = "true" ]; } \
|
|
95
|
+
&& _pass "version selection (default=25 @new, override=1 @old)" || _fail "version selection (new=$new_cost old=$old_cost)"
|
|
96
|
+
|
|
97
|
+
# --- 6. cross-runtime golden vectors ---------------------------------------
|
|
98
|
+
n=$(jq '.cases|length' "$GOLDEN")
|
|
99
|
+
for i in $(seq 0 $((n - 1))); do
|
|
100
|
+
c=$(jq ".cases[$i]" "$GOLDEN")
|
|
101
|
+
name=$(echo "$c" | jq -r '.name'); model=$(echo "$c" | jq -r '.model')
|
|
102
|
+
inp=$(echo "$c" | jq '.tokens.input'); out=$(echo "$c" | jq '.tokens.output')
|
|
103
|
+
cc=$(echo "$c" | jq '.tokens.cache_creation'); cr=$(echo "$c" | jq '.tokens.cache_read')
|
|
104
|
+
exp=$(echo "$c" | jq '.expected_cost_usd')
|
|
105
|
+
gtp="$(mktemp)"; mk_line "$model" "$inp" "$out" "$cc" "$cr" > "$gtp"
|
|
106
|
+
act=$(usage_parse_transcript "$gtp" | jq '.estimated_cost_usd')
|
|
107
|
+
if [ -n "$act" ] && [ "$(approx_eq "$act" "$exp")" = "true" ]; then
|
|
108
|
+
_pass "golden: $name ($model) = \$$exp"
|
|
109
|
+
else
|
|
110
|
+
_fail "golden: $name ($model) expected \$$exp got \$${act:-EMPTY}"
|
|
111
|
+
fi
|
|
112
|
+
rm -f "$gtp"
|
|
113
|
+
done
|
|
114
|
+
|
|
115
|
+
rm -f "$ov" "$bad" "$tp" "$empty" "$drift" "$dlog" "$tp2" "$v2" 2>/dev/null
|
|
116
|
+
|
|
117
|
+
echo ""
|
|
118
|
+
echo "Usage + cost: $pass passed, $fail failed"
|
|
119
|
+
[ "$fail" -eq 0 ]
|