@crewhaus/canary-controller 0.1.1 → 0.1.3
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/package.json +9 -14
- package/src/index.test.ts +42 -0
- package/src/index.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crewhaus/canary-controller",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Percent-of-traffic rollout with eval-gated promotion. Hash-routes requests across two pinned versions and auto-rolls-back on regression.",
|
|
6
6
|
"main": "src/index.ts",
|
|
@@ -12,16 +12,16 @@
|
|
|
12
12
|
"test": "bun test src"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@crewhaus/audit-log": "0.1.
|
|
16
|
-
"@crewhaus/deployment-controller": "0.1.
|
|
17
|
-
"@crewhaus/errors": "0.1.
|
|
18
|
-
"@crewhaus/spec-registry": "0.1.
|
|
15
|
+
"@crewhaus/audit-log": "0.1.3",
|
|
16
|
+
"@crewhaus/deployment-controller": "0.1.3",
|
|
17
|
+
"@crewhaus/errors": "0.1.3",
|
|
18
|
+
"@crewhaus/spec-registry": "0.1.3"
|
|
19
19
|
},
|
|
20
20
|
"license": "Apache-2.0",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Max Meier",
|
|
23
|
-
"email": "max@
|
|
24
|
-
"url": "https://
|
|
23
|
+
"email": "max@crewhaus.ai",
|
|
24
|
+
"url": "https://crewhaus.ai"
|
|
25
25
|
},
|
|
26
26
|
"repository": {
|
|
27
27
|
"type": "git",
|
|
@@ -33,12 +33,7 @@
|
|
|
33
33
|
"url": "https://github.com/crewhaus/factory/issues"
|
|
34
34
|
},
|
|
35
35
|
"publishConfig": {
|
|
36
|
-
"access": "
|
|
36
|
+
"access": "public"
|
|
37
37
|
},
|
|
38
|
-
"files": [
|
|
39
|
-
"src",
|
|
40
|
-
"README.md",
|
|
41
|
-
"LICENSE",
|
|
42
|
-
"NOTICE"
|
|
43
|
-
]
|
|
38
|
+
"files": ["src", "README.md", "LICENSE", "NOTICE"]
|
|
44
39
|
}
|
package/src/index.test.ts
CHANGED
|
@@ -90,6 +90,48 @@ describe("canary-controller — T3 traffic routing", () => {
|
|
|
90
90
|
ctrl.route({ name: "x", fromVersion: "v1", toVersion: "v2", trafficPercent: 150 }, "r"),
|
|
91
91
|
).toThrow(CanaryError);
|
|
92
92
|
});
|
|
93
|
+
|
|
94
|
+
test("negative trafficPercent throws", async () => {
|
|
95
|
+
const reg = createFileBackedRegistry({ rootDir: join(tmpRoot, "specs") });
|
|
96
|
+
const deploy = createDeploymentController({ registry: reg });
|
|
97
|
+
const ctrl = createCanaryController({ registry: reg, deploymentController: deploy });
|
|
98
|
+
expect(() =>
|
|
99
|
+
ctrl.route({ name: "x", fromVersion: "v1", toVersion: "v2", trafficPercent: -1 }, "r"),
|
|
100
|
+
).toThrow(CanaryError);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("NaN trafficPercent throws instead of silently routing all traffic to control", async () => {
|
|
104
|
+
// Regression: `NaN < 0` and `NaN > 100` are both false, so without an
|
|
105
|
+
// explicit finiteness check a NaN percentage slips past validation and
|
|
106
|
+
// every request silently falls back to fromVersion (isCanary always false).
|
|
107
|
+
const reg = createFileBackedRegistry({ rootDir: join(tmpRoot, "specs") });
|
|
108
|
+
const deploy = createDeploymentController({ registry: reg });
|
|
109
|
+
const ctrl = createCanaryController({ registry: reg, deploymentController: deploy });
|
|
110
|
+
expect(() =>
|
|
111
|
+
ctrl.route(
|
|
112
|
+
{ name: "x", fromVersion: "v1", toVersion: "v2", trafficPercent: Number.NaN },
|
|
113
|
+
"r",
|
|
114
|
+
),
|
|
115
|
+
).toThrow(CanaryError);
|
|
116
|
+
expect(() =>
|
|
117
|
+
ctrl.route(
|
|
118
|
+
{ name: "x", fromVersion: "v1", toVersion: "v2", trafficPercent: Number.NaN },
|
|
119
|
+
"r",
|
|
120
|
+
),
|
|
121
|
+
).toThrow(/trafficPercent must be in 0\.\.100/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("Infinity trafficPercent throws", async () => {
|
|
125
|
+
const reg = createFileBackedRegistry({ rootDir: join(tmpRoot, "specs") });
|
|
126
|
+
const deploy = createDeploymentController({ registry: reg });
|
|
127
|
+
const ctrl = createCanaryController({ registry: reg, deploymentController: deploy });
|
|
128
|
+
expect(() =>
|
|
129
|
+
ctrl.route(
|
|
130
|
+
{ name: "x", fromVersion: "v1", toVersion: "v2", trafficPercent: Number.POSITIVE_INFINITY },
|
|
131
|
+
"r",
|
|
132
|
+
),
|
|
133
|
+
).toThrow(CanaryError);
|
|
134
|
+
});
|
|
93
135
|
});
|
|
94
136
|
|
|
95
137
|
describe("canary-controller — eval gate", () => {
|
package/src/index.ts
CHANGED
|
@@ -92,7 +92,11 @@ export type CanaryControllerOptions = {
|
|
|
92
92
|
export function createCanaryController(opts: CanaryControllerOptions): CanaryController {
|
|
93
93
|
return {
|
|
94
94
|
route(config, requestId): CanaryRoutingDecision {
|
|
95
|
-
if (
|
|
95
|
+
if (
|
|
96
|
+
!Number.isFinite(config.trafficPercent) ||
|
|
97
|
+
config.trafficPercent < 0 ||
|
|
98
|
+
config.trafficPercent > 100
|
|
99
|
+
) {
|
|
96
100
|
throw new CanaryError(`trafficPercent must be in 0..100; got ${config.trafficPercent}`);
|
|
97
101
|
}
|
|
98
102
|
const bucket = computeBucket(config.tenantId, requestId);
|