@intentius/chant 0.65.0 → 0.66.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/dist/behaviour-delta.d.ts +181 -0
- package/dist/behaviour-delta.d.ts.map +1 -0
- package/dist/behaviour-http.d.ts +106 -0
- package/dist/behaviour-http.d.ts.map +1 -0
- package/dist/behaviour-overlay.d.ts +61 -0
- package/dist/behaviour-overlay.d.ts.map +1 -0
- package/dist/behaviour.d.ts +178 -3
- package/dist/behaviour.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lifecycle/scenario-eval.d.ts +23 -5
- package/dist/lifecycle/scenario-eval.d.ts.map +1 -1
- package/dist/lifecycle/scenario.d.ts +45 -3
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/types.d.ts +17 -0
- package/dist/lifecycle/types.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +42 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/predict-behaviour.d.ts +207 -0
- package/dist/op/activities/predict-behaviour.d.ts.map +1 -0
- package/dist/op/activities/reconcile.d.ts +7 -0
- package/dist/op/activities/reconcile.d.ts.map +1 -1
- package/dist/op/composites/behaviour-op.d.ts +57 -0
- package/dist/op/composites/behaviour-op.d.ts.map +1 -0
- package/dist/op/composites/index.d.ts +2 -0
- package/dist/op/composites/index.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/behaviour-delta.test.ts +331 -0
- package/src/behaviour-delta.ts +564 -0
- package/src/behaviour-http.test.ts +456 -0
- package/src/behaviour-http.ts +252 -0
- package/src/behaviour-overlay.test.ts +149 -0
- package/src/behaviour-overlay.ts +76 -0
- package/src/behaviour.test.ts +50 -0
- package/src/behaviour.ts +255 -3
- package/src/cli/handlers/scenario.test.ts +108 -0
- package/src/cli/handlers/scenario.ts +63 -16
- package/src/index.ts +2 -0
- package/src/lifecycle/scenario-cost.test.ts +183 -0
- package/src/lifecycle/scenario-eval.ts +133 -6
- package/src/lifecycle/scenario.ts +72 -4
- package/src/lifecycle/types.ts +17 -0
- package/src/lint/rules/op/ops012-activity-contract.test.ts +31 -0
- package/src/op/activities/activity-contracts.ts +49 -0
- package/src/op/activities/index.ts +24 -0
- package/src/op/activities/predict-behaviour.test.ts +255 -0
- package/src/op/activities/predict-behaviour.ts +468 -0
- package/src/op/activities/reconcile.ts +7 -2
- package/src/op/activity-contract-registry.test.ts +3 -0
- package/src/op/composites/behaviour-op.test.ts +56 -0
- package/src/op/composites/behaviour-op.ts +99 -0
- package/src/op/composites/index.ts +2 -0
- package/src/op/index.ts +2 -0
package/dist/op/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/op/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,iBAAiB,EAAE,YAAY,EAAE,YAAY,EAClH,cAAc,EAAE,iBAAiB,EAAE,KAAK,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,KAAK,EAAE,OAAO,EAC7F,UAAU,EAAE,YAAY,EAAE,OAAO,EAAE,SAAS,EAC5C,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAC3D,aAAa,EAAE,aAAa,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAE,UAAU,EACrG,aAAa,EACb,YAAY,EAAE,UAAU,EAAE,gBAAgB,EAAE,aAAa,EAAE,eAAe,EAAE,aAAa,EACzF,eAAe,EAAE,cAAc,EAAE,aAAa,EAAE,YAAY,EAC5D,wBAAwB,EAAE,mBAAmB,EAC7C,gBAAgB,EAAE,iBAAiB,EAAE,iBAAiB,EACtD,SAAS,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AACxC,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvF,YAAY,EAAE,YAAY,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACrI,OAAO,EAAE,cAAc,EAAE,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AACpH,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAC5D,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,cAAc,EAAE,YAAY,EAAE,QAAQ,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AAC3I,OAAO,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,iBAAiB,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,QAAQ,CAAC;AAC/F,OAAO,EACL,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,UAAU,EACzC,eAAe,EAAE,eAAe,EAAE,gBAAgB,EAAE,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/op/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The predicted delta and its rendering (#2358).
|
|
3
|
+
*
|
|
4
|
+
* Every rule here is the contract's or the #2358 comment's, and each has a
|
|
5
|
+
* test that fails when the rule is dropped: a declined entity as a row, a
|
|
6
|
+
* mismatched pair as a mark, a refusal as "no prediction", provenance on
|
|
7
|
+
* every figure, a rate as a rate, and resilience uncompared across unequal
|
|
8
|
+
* edge coverage.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, expect, test } from "vitest";
|
|
12
|
+
import {
|
|
13
|
+
behaviourReport,
|
|
14
|
+
noBehaviourEngineRefusal,
|
|
15
|
+
predictedRate,
|
|
16
|
+
renderBehaviourRefusal,
|
|
17
|
+
unreachableBehaviourEngineRefusal,
|
|
18
|
+
type BehaviourEdgeCoverage,
|
|
19
|
+
type BehaviourResult,
|
|
20
|
+
type PredictedBehaviour,
|
|
21
|
+
type UnpredictedEntity,
|
|
22
|
+
} from "./behaviour";
|
|
23
|
+
import {
|
|
24
|
+
behaviourDelta,
|
|
25
|
+
formatPerHour,
|
|
26
|
+
renderBehaviourFinding,
|
|
27
|
+
renderProvenance,
|
|
28
|
+
renderRate,
|
|
29
|
+
validateBehaviourResult,
|
|
30
|
+
type BehaviourDeltaReport,
|
|
31
|
+
} from "./behaviour-delta";
|
|
32
|
+
|
|
33
|
+
const TRAFFIC = "100 rps, p50";
|
|
34
|
+
const COMPLETE: BehaviourEdgeCoverage = { verdict: "complete" };
|
|
35
|
+
const PARTIAL: BehaviourEdgeCoverage = { verdict: "partial", unresolvedKinds: ["AWS::EC2::VPC"] };
|
|
36
|
+
|
|
37
|
+
function figure(overrides: Partial<PredictedBehaviour> = {}): PredictedBehaviour {
|
|
38
|
+
return {
|
|
39
|
+
at: { traffic: TRAFFIC },
|
|
40
|
+
cost: predictedRate(0.272, "USD"),
|
|
41
|
+
headroom: { cpu: 0.35, latency: 0.28 },
|
|
42
|
+
errorRate: 0.0005,
|
|
43
|
+
resilience: { failure: "one zone lost", verdict: "degrades" },
|
|
44
|
+
provenance: { engine: "acme-sim", version: "1.4.2", tolerance: "±15%", basis: "modeled" },
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function report(
|
|
50
|
+
entities: Record<string, PredictedBehaviour>,
|
|
51
|
+
unpredicted: Record<string, UnpredictedEntity> = {},
|
|
52
|
+
opts: { coverage?: BehaviourEdgeCoverage; traffic?: string; total?: { perHour: number; currency: string } } = {},
|
|
53
|
+
): BehaviourResult {
|
|
54
|
+
const traffic = opts.traffic ?? TRAFFIC;
|
|
55
|
+
const priced = Object.fromEntries(
|
|
56
|
+
Object.entries(entities).map(([name, f]) => [name, { ...f, at: { traffic } }]),
|
|
57
|
+
);
|
|
58
|
+
return behaviourReport(
|
|
59
|
+
{ entityNames: [...Object.keys(entities), ...Object.keys(unpredicted)], traffic, edgeCoverage: opts.coverage ?? COMPLETE },
|
|
60
|
+
{ engine: "acme-sim", version: "1.4.2", ...(opts.total ? { total: predictedRate(opts.total.perHour, opts.total.currency) } : {}) },
|
|
61
|
+
priced,
|
|
62
|
+
unpredicted,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const CTX = { env: "prod", op: "pr-behaviour" };
|
|
67
|
+
|
|
68
|
+
function delta(base: BehaviourResult, head: BehaviourResult) {
|
|
69
|
+
return behaviourDelta({ label: "base", ref: "main", result: base }, { label: "head", ref: "feature", result: head });
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** The words the finding must never contain, as whole words. */
|
|
73
|
+
const FORBIDDEN = [/\bbills?\b/i, /\binvoices?\b/i, /\bcharges?\b/i, /\bcost you\b/i];
|
|
74
|
+
|
|
75
|
+
describe("validateBehaviourResult — a result held to the contract on arrival", () => {
|
|
76
|
+
test("accepts a report the builder made, with the names it was asked", () => {
|
|
77
|
+
const r = report({ db: figure() }, { role: { type: "AWS::IAM::Role", reason: "unsupported-kind" } });
|
|
78
|
+
expect(validateBehaviourResult(r, ["db", "role"])).toBe(r);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("accepts a refusal with a legal cause, a reason and a remedy", () => {
|
|
82
|
+
const r = noBehaviourEngineRefusal("augur");
|
|
83
|
+
expect(validateBehaviourResult(r, ["db"])).toBe(r);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("refuses a bare envelope that is neither arm", () => {
|
|
87
|
+
expect(() => validateBehaviourResult({ behaviour: "v1" }, [])).toThrow(/neither a report nor a refusal/);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("refuses a report that gives no verdict for a name it was asked", () => {
|
|
91
|
+
const r = report({ db: figure() });
|
|
92
|
+
expect(() => validateBehaviourResult(r, ["db", "orders"])).toThrow(/no verdict at all for "orders"/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("refuses a figure for a name nobody asked about", () => {
|
|
96
|
+
const r = report({ db: figure() });
|
|
97
|
+
expect(() => validateBehaviourResult(r, [])).toThrow(/"db", which was not asked about/);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("refuses a hand-built block that fails the block validator — a negative rate", () => {
|
|
101
|
+
const r = report({ db: figure() }) as { entities: Record<string, PredictedBehaviour> };
|
|
102
|
+
const tampered = { ...r, entities: { db: { ...r.entities.db, cost: predictedRate(-1, "USD") } } };
|
|
103
|
+
expect(() => validateBehaviourResult(tampered, ["db"])).toThrow(/cost.perHour is negative/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("refuses a refusal with a cause outside the closed set", () => {
|
|
107
|
+
const r = { behaviour: "v1", refusal: { cause: "no-money", reason: "x", remedy: "y" } };
|
|
108
|
+
expect(() => validateBehaviourResult(r, [])).toThrow(/not a legal reason/);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe("behaviourDelta — a whole-run refusal on either side is no prediction", () => {
|
|
113
|
+
test("a refused base side yields no-prediction carrying that side's refusal, and no rows", () => {
|
|
114
|
+
const d = delta(noBehaviourEngineRefusal("augur"), report({ db: figure() }));
|
|
115
|
+
expect(d.kind).toBe("no-prediction");
|
|
116
|
+
if (d.kind !== "no-prediction") return;
|
|
117
|
+
expect(d.base.refusal?.cause).toBe("no-engine");
|
|
118
|
+
expect(d.head.refusal).toBeUndefined();
|
|
119
|
+
expect("rows" in d).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("a refused head side likewise, and both when both refuse", () => {
|
|
123
|
+
const down = unreachableBehaviourEngineRefusal("augur", { value: "engine", source: "CHANT_BEHAVIOUR_ENGINE" }, "ECONNREFUSED");
|
|
124
|
+
const d = delta(report({ db: figure() }), down);
|
|
125
|
+
expect(d.kind).toBe("no-prediction");
|
|
126
|
+
const both = delta(noBehaviourEngineRefusal("augur"), down);
|
|
127
|
+
if (both.kind !== "no-prediction") throw new Error("expected no-prediction");
|
|
128
|
+
expect(both.base.refusal?.cause).toBe("no-engine");
|
|
129
|
+
expect(both.head.refusal?.cause).toBe("engine-unreachable");
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the rendered finding says no prediction, carries renderBehaviourRefusal's text and the remedy, and no figure", () => {
|
|
133
|
+
const d = delta(noBehaviourEngineRefusal("augur"), report({ db: figure() }));
|
|
134
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
135
|
+
expect(body).toContain("no prediction");
|
|
136
|
+
const refusal = noBehaviourEngineRefusal("augur").refusal;
|
|
137
|
+
expect(body).toContain(renderBehaviourRefusal(refusal, { color: false }));
|
|
138
|
+
expect(body).toContain(refusal.remedy);
|
|
139
|
+
expect(body).not.toContain("/hour");
|
|
140
|
+
expect(body).not.toContain("| Entity |");
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("behaviourDelta — a declined entity is a row, not an absent finding and not a zero", () => {
|
|
145
|
+
const base = report({ db: figure(), orders: figure({ cost: predictedRate(0.004, "USD") }) });
|
|
146
|
+
const head = report(
|
|
147
|
+
{ db: figure() },
|
|
148
|
+
{ orders: { type: "AWS::SQS::Queue", reason: "unsupported-kind", detail: "AWS::SQS::Queue is declared unmapped by the augur coverage table" } },
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
test("the declined side is a row of kind declined carrying the reason, with no delta number", () => {
|
|
152
|
+
const d = delta(base, head) as BehaviourDeltaReport;
|
|
153
|
+
expect(d.kind).toBe("delta");
|
|
154
|
+
const row = d.rows.find((r) => r.name === "orders")!;
|
|
155
|
+
expect(row.kind).toBe("declined");
|
|
156
|
+
expect(row.headDeclined?.reason).toBe("unsupported-kind");
|
|
157
|
+
expect(row.base?.cost.perHour).toBe(0.004);
|
|
158
|
+
expect(row.deltaPerHour).toBeUndefined();
|
|
159
|
+
expect(row.type).toBe("AWS::SQS::Queue");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("the sum of comparable deltas excludes the declined entity", () => {
|
|
163
|
+
const d = delta(base, head) as BehaviourDeltaReport;
|
|
164
|
+
expect(d.sums).toEqual([{ currency: "USD", perHour: 0, pairs: 1 }]);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("an estate that gained an unmapped kind is not a cost change", () => {
|
|
168
|
+
const grown = report(
|
|
169
|
+
{ db: figure() },
|
|
170
|
+
{ role: { type: "AWS::IAM::Role", reason: "unsupported-kind", detail: "a role is a grant" } },
|
|
171
|
+
);
|
|
172
|
+
const d = delta(report({ db: figure() }), grown) as BehaviourDeltaReport;
|
|
173
|
+
expect(d.rows.find((r) => r.name === "role")?.kind).toBe("declined");
|
|
174
|
+
expect(d.sums).toEqual([{ currency: "USD", perHour: 0, pairs: 1 }]);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("renders the declined row in the table and its detail under it", () => {
|
|
178
|
+
const body = renderBehaviourFinding(delta(base, head), CTX);
|
|
179
|
+
expect(body).toMatch(/\| orders \| AWS::SQS::Queue \| 0\.004 USD\/hour \| declined: unsupported-kind \| no delta \(declined\) \|/);
|
|
180
|
+
expect(body).toContain("### Declined entities");
|
|
181
|
+
expect(body).toContain("`orders` on head: `unsupported-kind` — AWS::SQS::Queue is declared unmapped");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe("behaviourDelta — a mismatched pair is marked, never subtracted", () => {
|
|
186
|
+
test("mixed-basis: the row is marked with the label and carries no delta number", () => {
|
|
187
|
+
const base = report({ db: figure() });
|
|
188
|
+
const head = report({
|
|
189
|
+
db: figure({ provenance: { engine: "acme-sim", version: "1.4.2", tolerance: "±15%", basis: "validated" } }),
|
|
190
|
+
});
|
|
191
|
+
const d = delta(base, head) as BehaviourDeltaReport;
|
|
192
|
+
const row = d.rows[0];
|
|
193
|
+
expect(row.kind).toBe("marked");
|
|
194
|
+
expect(row.mismatches).toEqual(["mixed-basis"]);
|
|
195
|
+
expect(row.deltaPerHour).toBeUndefined();
|
|
196
|
+
expect(d.sums).toEqual([]);
|
|
197
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
198
|
+
expect(body).toMatch(/\| db \| \| 0\.272 USD\/hour \| 0\.272 USD\/hour \| marked: mixed-basis \|/);
|
|
199
|
+
expect(body).toContain("No pair is comparable, so no delta is summed.");
|
|
200
|
+
expect(body).toContain("base: acme-sim 1.4.2 · ±15% · modeled; head: acme-sim 1.4.2 · ±15% · validated");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("every mismatched axis is named, in the contract's display order", () => {
|
|
204
|
+
const base = report({ db: figure() });
|
|
205
|
+
const head = report(
|
|
206
|
+
{
|
|
207
|
+
db: figure({
|
|
208
|
+
cost: predictedRate(0.3, "EUR"),
|
|
209
|
+
resilience: { failure: "a region lost", verdict: "fails" },
|
|
210
|
+
provenance: { engine: "other-sim", version: "9", tolerance: "±50%", basis: "validated" },
|
|
211
|
+
}),
|
|
212
|
+
},
|
|
213
|
+
{},
|
|
214
|
+
{ traffic: "1000 rps, p99" },
|
|
215
|
+
);
|
|
216
|
+
const d = delta(base, head) as BehaviourDeltaReport;
|
|
217
|
+
expect(d.rows[0].mismatches).toEqual(["mixed-engine", "mixed-level", "mixed-currency", "mixed-basis", "mixed-failure"]);
|
|
218
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
219
|
+
expect(body).toContain("marked: mixed-engine, mixed-level, mixed-currency, mixed-basis, mixed-failure");
|
|
220
|
+
expect(body).toContain("predicted at different traffic levels");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("a comparable pair is a plain difference, summed per currency", () => {
|
|
224
|
+
const base = report({ db: figure(), eu: figure({ cost: predictedRate(1, "EUR") }) });
|
|
225
|
+
const head = report({ db: figure({ cost: predictedRate(0.4, "USD") }), eu: figure({ cost: predictedRate(0.5, "EUR") }) });
|
|
226
|
+
const d = delta(base, head) as BehaviourDeltaReport;
|
|
227
|
+
expect(d.rows.map((r) => [r.name, r.kind, r.deltaPerHour])).toEqual([
|
|
228
|
+
["db", "comparable", 0.4 - 0.272],
|
|
229
|
+
["eu", "comparable", -0.5],
|
|
230
|
+
]);
|
|
231
|
+
expect(d.sums).toEqual([
|
|
232
|
+
{ currency: "EUR", perHour: -0.5, pairs: 1 },
|
|
233
|
+
{ currency: "USD", perHour: 0.4 - 0.272, pairs: 1 },
|
|
234
|
+
]);
|
|
235
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
236
|
+
expect(body).toContain("| +0.128 USD/hour |");
|
|
237
|
+
expect(body).toContain("| -0.5 EUR/hour |");
|
|
238
|
+
expect(body).toContain("chant's own arithmetic");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("an entity on one side only is added or removed, with no delta", () => {
|
|
242
|
+
const d = delta(report({ db: figure(), old: figure() }), report({ db: figure(), fresh: figure() })) as BehaviourDeltaReport;
|
|
243
|
+
expect(d.rows.find((r) => r.name === "old")?.kind).toBe("only-base");
|
|
244
|
+
expect(d.rows.find((r) => r.name === "fresh")?.kind).toBe("only-head");
|
|
245
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
246
|
+
expect(body).toMatch(/\| old \|.*\| removed \|/);
|
|
247
|
+
expect(body).toMatch(/\| fresh \|.*\| added \|/);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe("renderBehaviourFinding — every figure carries its provenance, and a rate is a rate", () => {
|
|
252
|
+
test("a rate renders per hour, and never as an amount", () => {
|
|
253
|
+
expect(renderRate(predictedRate(0.272, "USD"))).toBe("0.272 USD/hour");
|
|
254
|
+
expect(renderRate(predictedRate(3, "EUR"))).toBe("3 EUR/hour");
|
|
255
|
+
expect(formatPerHour(0.00230000001)).toBe("0.0023");
|
|
256
|
+
expect(formatPerHour(12.5)).toBe("12.5");
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("provenance is the four fields together", () => {
|
|
260
|
+
expect(renderProvenance(figure().provenance)).toBe("acme-sim 1.4.2 · ±15% · modeled");
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
test("each row carries the engine, version, tolerance and basis of its figures", () => {
|
|
264
|
+
const d = delta(report({ db: figure(), web: figure({ cost: predictedRate(0.04, "USD") }) }), report({ db: figure(), web: figure({ cost: predictedRate(0.05, "USD") }) }));
|
|
265
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
266
|
+
const rows = body.split("\n").filter((l) => /^\| (db|web) \|/.test(l));
|
|
267
|
+
expect(rows).toHaveLength(2);
|
|
268
|
+
for (const row of rows) expect(row).toContain("acme-sim 1.4.2 · ±15% · modeled");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("names the Op and the env in the heading, and the traffic level", () => {
|
|
272
|
+
const body = renderBehaviourFinding(delta(report({ db: figure() }), report({ db: figure() })), CTX);
|
|
273
|
+
expect(body).toContain("## Predicted behaviour for `prod` at `100 rps, p50` (Op `pr-behaviour`)");
|
|
274
|
+
expect(body).toContain("A prediction, not a measurement.");
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("never uses the words that would make a prediction read as money owed", () => {
|
|
278
|
+
const withEverything = delta(
|
|
279
|
+
report({ db: figure(), orders: figure({ cost: predictedRate(0.004, "USD") }), old: figure() }, {}, { total: { perHour: 0.3, currency: "USD" } }),
|
|
280
|
+
report(
|
|
281
|
+
{ db: figure({ provenance: { engine: "acme-sim", version: "1.4.2", tolerance: "±15%", basis: "validated" }, rightSize: { suggestion: "db.t4g.micro", reason: "headroom" } }), fresh: figure() },
|
|
282
|
+
{ orders: { type: "AWS::SQS::Queue", reason: "unsupported-kind", detail: "declared unmapped" } },
|
|
283
|
+
{ coverage: PARTIAL },
|
|
284
|
+
),
|
|
285
|
+
);
|
|
286
|
+
for (const body of [
|
|
287
|
+
renderBehaviourFinding(withEverything, CTX),
|
|
288
|
+
renderBehaviourFinding(delta(noBehaviourEngineRefusal("augur"), report({ db: figure() })), CTX),
|
|
289
|
+
]) {
|
|
290
|
+
for (const word of FORBIDDEN) expect(body).not.toMatch(word);
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("shows the engine's own estate total per side and never differences the two", () => {
|
|
295
|
+
const d = delta(
|
|
296
|
+
report({ db: figure() }, {}, { total: { perHour: 0.3, currency: "USD" } }),
|
|
297
|
+
report({ db: figure() }, {}, { total: { perHour: 0.9, currency: "USD" } }),
|
|
298
|
+
);
|
|
299
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
300
|
+
expect(body).toContain("| base | main | acme-sim 1.4.2 | 100 rps, p50 | complete | 0.3 USD/hour |");
|
|
301
|
+
expect(body).toContain("| head | feature | acme-sim 1.4.2 | 100 rps, p50 | complete | 0.9 USD/hour |");
|
|
302
|
+
expect(body).not.toContain("0.6 USD/hour");
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("renderBehaviourFinding — resilience across unequal edge coverage", () => {
|
|
307
|
+
test("both sides complete: the verdicts are compared on one row", () => {
|
|
308
|
+
const d = delta(report({ db: figure() }), report({ db: figure({ resilience: { failure: "one zone lost", verdict: "fails" } }) })) as BehaviourDeltaReport;
|
|
309
|
+
expect(d.resilienceComparable).toBe(true);
|
|
310
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
311
|
+
expect(body).toContain("| degrades → fails (one zone lost) |");
|
|
312
|
+
expect(body).not.toContain("not compared");
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
test("either side partial or unknown: coverage is shown for both and the verdicts are shown per side, not compared", () => {
|
|
316
|
+
const d = delta(
|
|
317
|
+
report({ db: figure() }, {}, { coverage: PARTIAL }),
|
|
318
|
+
report({ db: figure({ resilience: { failure: "one zone lost", verdict: "fails" } }) }),
|
|
319
|
+
) as BehaviourDeltaReport;
|
|
320
|
+
expect(d.resilienceComparable).toBe(false);
|
|
321
|
+
const body = renderBehaviourFinding(d, CTX);
|
|
322
|
+
expect(body).toContain("| partial (unresolved kinds: AWS::EC2::VPC) |");
|
|
323
|
+
expect(body).toContain("| complete |");
|
|
324
|
+
expect(body).toContain("Resilience verdicts are shown per side and **not compared**");
|
|
325
|
+
expect(body).toContain("| base degrades (one zone lost); head fails (one zone lost) |");
|
|
326
|
+
expect(body).not.toContain("degrades → fails");
|
|
327
|
+
|
|
328
|
+
const unknown = delta(report({ db: figure() }), report({ db: figure() }, {}, { coverage: { verdict: "unknown" } })) as BehaviourDeltaReport;
|
|
329
|
+
expect(unknown.resilienceComparable).toBe(false);
|
|
330
|
+
});
|
|
331
|
+
});
|