@crewhaus/canary-controller 0.1.1 → 0.1.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/canary-controller",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
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.1",
16
- "@crewhaus/deployment-controller": "0.1.1",
17
- "@crewhaus/errors": "0.1.1",
18
- "@crewhaus/spec-registry": "0.1.1"
15
+ "@crewhaus/audit-log": "0.1.2",
16
+ "@crewhaus/deployment-controller": "0.1.2",
17
+ "@crewhaus/errors": "0.1.2",
18
+ "@crewhaus/spec-registry": "0.1.2"
19
19
  },
20
20
  "license": "Apache-2.0",
21
21
  "author": {
22
22
  "name": "Max Meier",
23
- "email": "max@studiomax.io",
24
- "url": "https://studiomax.io"
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": "restricted"
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 (config.trafficPercent < 0 || config.trafficPercent > 100) {
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);