First public release
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
dist/
|
||||
*.log
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
.npmrc
|
||||
dist-cjs/
|
||||
@@ -0,0 +1,6 @@
|
||||
stages: [test]
|
||||
|
||||
test:
|
||||
stage: test
|
||||
image: node:22-alpine
|
||||
script: [npm ci, npm run build, npm test]
|
||||
@@ -0,0 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0 - 2026-09-28
|
||||
|
||||
First public release under MIT.
|
||||
|
||||
- `checkPlan()` and `formatPlan()` check a deploy plan against a total-volume
|
||||
budget, a peak-rate ceiling and a destination allowlist.
|
||||
- `worstCaseBytes()` counts every retry attempt.
|
||||
- A step reaching a host outside the allowlist is critical however small the
|
||||
transfer.
|
||||
- Peak rate is checked separately from total and multiplied by concurrency.
|
||||
- An unmeasured step is NOT ASSESSED and makes the total a stated lower
|
||||
bound. The total still fails when the known bytes alone exceed the budget.
|
||||
@@ -0,0 +1,21 @@
|
||||
# Contributing
|
||||
|
||||
Issues and merge requests are welcome at
|
||||
https://gitlab.com/extant2000/quiet-deploy.
|
||||
|
||||
## Ground rules
|
||||
|
||||
- A test that cannot fail proves nothing. If you fix a bug, add a test and
|
||||
check that it fails against the unfixed code before you submit.
|
||||
- Measure, don't assume. Two modules with the same line count can still be
|
||||
different programs.
|
||||
- Keep dependencies minimal. Every new runtime dependency needs a reason.
|
||||
- Explain why in comments, not what. The what is already in the code.
|
||||
- A breaking change needs a major version bump and a note in CHANGELOG.md.
|
||||
|
||||
## Before you open a merge request
|
||||
|
||||
Run both of these and make sure they pass:
|
||||
|
||||
npm run build
|
||||
npm test
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Extant 2000 LLC
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,89 @@
|
||||
# QuietDeploy
|
||||
|
||||
Check a deployment plan against a network-signature budget before it runs.
|
||||
|
||||
QuietDeploy takes the steps of a rollout (what each one contacts, how many
|
||||
bytes it moves, for how long, with how many retries) and checks them against
|
||||
a total-volume budget, a peak-rate ceiling and a destination allowlist. It is
|
||||
pure, with no network and no clock. It sends nothing.
|
||||
|
||||
## What it checks and why
|
||||
|
||||
A deployment is observable. On a constrained or monitored link, the volume,
|
||||
the destinations and the timing of what a rollout pulls are a signal in
|
||||
themselves. The failures tend to be the same three.
|
||||
|
||||
1. The pull nobody counted. A build that "just pulls a base image" can move
|
||||
hundreds of megabytes to a third-party registry. It does not show up in
|
||||
the plan because nobody wrote it as a step; it is a side effect of one. A
|
||||
step with `bytes: null` is NOT ASSESSED, never zero. An unmeasured
|
||||
transfer is not a small one.
|
||||
2. Egress outside the declared set. An allowlist exists because a deploy
|
||||
that reaches an unexpected host is either a supply-chain problem or a
|
||||
leak. That check runs first and stays critical however small the transfer
|
||||
is. Volume is irrelevant to it.
|
||||
3. A burst that fits the total and not any window. Ten targets pulling 200 MB
|
||||
each inside a minute is a different signature from the same traffic over
|
||||
an hour, and a total-only budget passes both. Peak rate is checked on its
|
||||
own and multiplied by `concurrency`.
|
||||
|
||||
Retries are cost. `worstCaseBytes()` counts every attempt, because a step
|
||||
budgeted at one attempt is budgeted at its best case. A plan that fits on the
|
||||
first try and not with retries fails.
|
||||
|
||||
When any step is unmeasured, the total is reported as a lower bound, not a
|
||||
total. A partial sum presented as a total is how a budget check reassures
|
||||
without checking. The total still fails if the known bytes alone exceed the
|
||||
budget, since unmeasured steps cannot rescue a plan that is already over.
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import { checkPlan, formatPlan, type DeployStep, type SignatureBudget } from '@extant2000/quiet-deploy'
|
||||
|
||||
const steps: DeployStep[] = [
|
||||
{ name: 'pull-base-image', destination: 'registry.example.com', bytes: 200_000_000, durationSeconds: 60, retries: 2, where: 'ci.yml' },
|
||||
{ name: 'fetch-cli', destination: 'downloads.example.net', bytes: 5_000_000, durationSeconds: 10, where: 'scripts/setup.sh' },
|
||||
{ name: 'push-app-image', destination: 'registry.example.com', bytes: null, where: 'ci.yml' },
|
||||
]
|
||||
|
||||
const budget: SignatureBudget = {
|
||||
maxTotalBytes: 2e9,
|
||||
maxBytesPerSecond: 5e6,
|
||||
allowedDestinations: ['registry.example.com'],
|
||||
concurrency: 4,
|
||||
}
|
||||
|
||||
const report = checkPlan(steps, budget)
|
||||
for (const f of report.findings) console.log(`${f.determination.padEnd(13)} ${f.id}: ${f.summary}`)
|
||||
|
||||
// The full evidence-record report for a terminal or CI log
|
||||
console.log(formatPlan(steps, budget))
|
||||
```
|
||||
|
||||
`checkPlan()` returns a `CapabilityReport` from `@extant2000/evidence-record`.
|
||||
`worstCaseBytes()`, `peakRate()` and `formatBytes()` are exported as well.
|
||||
|
||||
## Sample output
|
||||
|
||||
Produced by running the example above (the loop over findings):
|
||||
|
||||
```
|
||||
not-assessed total: 405.0 MB of 2.0 GB accounted for, with 1 step(s) unmeasured
|
||||
fail pull-base-image: pull-base-image peaks at 13.3 MB/s
|
||||
fail fetch-cli: fetch-cli contacts downloads.example.net, which is not in the declared set
|
||||
not-assessed push-app-image: push-app-image moves an unknown volume to registry.example.com
|
||||
```
|
||||
|
||||
The 405 MB total counts both attempts of the base-image pull. The pull fails
|
||||
the rate check only because four run at once.
|
||||
|
||||
## Install
|
||||
|
||||
```
|
||||
npm install @extant2000/quiet-deploy
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT. Copyright (c) 2026 Extant 2000 LLC. See [LICENSE](LICENSE).
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
# Security policy
|
||||
|
||||
## Reporting a vulnerability
|
||||
|
||||
Please report vulnerabilities privately by email to security@extant2000.com.
|
||||
Do not open a public issue or merge request for a security problem.
|
||||
|
||||
Include the affected version, a description of the issue, and steps or a test
|
||||
that reproduce it if you have them.
|
||||
|
||||
We aim to acknowledge every report within 5 business days.
|
||||
Generated
+1504
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@extant2000/quiet-deploy",
|
||||
"version": "0.1.0",
|
||||
"private": false,
|
||||
"description": "Deployment that holds a defined network-signature threshold.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"main": "./dist-cjs/index.js",
|
||||
"files": [
|
||||
"dist",
|
||||
"dist-cjs",
|
||||
"src",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://gitlab.com/extant2000/quiet-deploy.git"
|
||||
},
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js",
|
||||
"require": "./dist-cjs/index.js"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && tsc -p tsconfig.cjs.json && node -e \"require('fs').writeFileSync('dist-cjs/package.json', JSON.stringify({type:'commonjs'}))\"",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.0",
|
||||
"vitest": "^2.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@extant2000/evidence-record": "^0.1.2"
|
||||
},
|
||||
"author": "Extant 2000 LLC",
|
||||
"homepage": "https://gitlab.com/extant2000/quiet-deploy",
|
||||
"bugs": {
|
||||
"url": "https://gitlab.com/extant2000/quiet-deploy/-/issues"
|
||||
}
|
||||
}
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* QuietDeploy — check a deployment plan against a network-signature budget
|
||||
* before it runs.
|
||||
*
|
||||
* A deployment is observable. On a constrained or monitored link, the volume,
|
||||
* the destinations and the timing of what a rollout pulls are themselves a
|
||||
* signal, and the failures are consistently the same three:
|
||||
*
|
||||
* 1. **The unbudgeted pull nobody counted.** A build that "just pulls a base
|
||||
* image" moves hundreds of megabytes to a third-party registry. It is
|
||||
* invisible in the plan because it is not a step anyone wrote — it is a
|
||||
* consequence of one.
|
||||
*
|
||||
* 2. **Egress to destinations outside the declared set.** An allowlisted
|
||||
* egress firewall exists precisely because a deploy that reaches an
|
||||
* unexpected host is either a supply-chain problem or a leak. A plan
|
||||
* that cannot enumerate its destinations cannot be checked against one.
|
||||
*
|
||||
* 3. **A burst that fits the budget in total and not in any window.** Ten
|
||||
* targets pulling 200 MB each inside one minute is a very different
|
||||
* signature from the same traffic spread over an hour, and a total-only
|
||||
* budget passes both. Peak rate is checked separately from volume.
|
||||
*
|
||||
* `retry` is treated as multiplying its step's cost, not as a footnote: a
|
||||
* step budgeted at its first-attempt size is budgeted at its best case.
|
||||
*
|
||||
* Pure: no network, no clock. It sends nothing.
|
||||
*/
|
||||
|
||||
import {
|
||||
type CapabilityReport,
|
||||
type Finding,
|
||||
type FormatOptions,
|
||||
evidence,
|
||||
formatReport as renderReport,
|
||||
} from '@extant2000/evidence-record'
|
||||
|
||||
export interface DeployStep {
|
||||
name: string
|
||||
/** Host contacted. Checked against the allowlist. */
|
||||
destination: string
|
||||
/** Bytes moved per attempt. `null` when unknown — never assumed zero. */
|
||||
bytes: number | null
|
||||
/** Seconds this step occupies. Used for the peak-rate check. */
|
||||
durationSeconds?: number
|
||||
/** Maximum attempts. A step budgeted at one attempt is budgeted at best case. */
|
||||
retries?: number
|
||||
/** Where the step is defined — a CI file, a compose file, a script. */
|
||||
where: string
|
||||
}
|
||||
|
||||
export interface SignatureBudget {
|
||||
/** Total bytes the whole rollout may move. */
|
||||
maxTotalBytes?: number
|
||||
/** Peak bytes per second in any single step. */
|
||||
maxBytesPerSecond?: number
|
||||
/** Hosts this rollout is permitted to contact. */
|
||||
allowedDestinations?: string[]
|
||||
/** Steps that may run at once — concurrency multiplies the peak. */
|
||||
concurrency?: number
|
||||
}
|
||||
|
||||
const fmt = (n: number): string =>
|
||||
n >= 1e9 ? `${(n / 1e9).toFixed(1)} GB`
|
||||
: n >= 1e6 ? `${(n / 1e6).toFixed(1)} MB`
|
||||
: n >= 1e3 ? `${(n / 1e3).toFixed(1)} kB`
|
||||
: `${n} B`
|
||||
|
||||
/** Worst-case bytes for a step: every attempt counts. */
|
||||
export function worstCaseBytes(s: DeployStep): number | null {
|
||||
if (s.bytes === null) return null
|
||||
return s.bytes * Math.max(1, s.retries ?? 1)
|
||||
}
|
||||
|
||||
/** Peak bytes/second for a step, `null` when either input is missing. */
|
||||
export function peakRate(s: DeployStep): number | null {
|
||||
if (s.bytes === null || !s.durationSeconds) return null
|
||||
return Math.round(s.bytes / s.durationSeconds)
|
||||
}
|
||||
|
||||
function stepEvidence(s: DeployStep) {
|
||||
return evidence.file(s.where, undefined,
|
||||
s.bytes === null ? 'volume unknown' : fmt(worstCaseBytes(s)!),
|
||||
`${s.destination}${s.retries && s.retries > 1 ? `, up to ${s.retries} attempts` : ''}`)
|
||||
}
|
||||
|
||||
function stepFinding(s: DeployStep, budget: SignatureBudget): Finding {
|
||||
const id = s.name
|
||||
const allowed = budget.allowedDestinations
|
||||
|
||||
// Checked first: an unexpected destination is a different KIND of problem
|
||||
// from a large one, and it stays a finding however small the transfer is.
|
||||
if (allowed && allowed.length > 0 && !allowed.includes(s.destination)) {
|
||||
return {
|
||||
id,
|
||||
summary: `${s.name} contacts ${s.destination}, which is not in the declared set`,
|
||||
determination: 'fail',
|
||||
severity: 'critical',
|
||||
detail: 'An egress allowlist exists because a deploy reaching an unexpected host is either a supply-chain problem or a leak. Volume is irrelevant here.',
|
||||
evidence: [stepEvidence(s)],
|
||||
}
|
||||
}
|
||||
|
||||
if (s.bytes === null) {
|
||||
return {
|
||||
id,
|
||||
summary: `${s.name} moves an unknown volume to ${s.destination}`,
|
||||
determination: 'not-assessed',
|
||||
severity: 'high',
|
||||
detail: 'An unmeasured transfer is not a small one. It cannot be counted against the budget, so the budget below is a lower bound, not a total.',
|
||||
evidence: [stepEvidence(s)],
|
||||
}
|
||||
}
|
||||
|
||||
const rate = peakRate(s)
|
||||
if (rate !== null && budget.maxBytesPerSecond && rate * (budget.concurrency ?? 1) > budget.maxBytesPerSecond) {
|
||||
return {
|
||||
id,
|
||||
summary: `${s.name} peaks at ${fmt(rate * (budget.concurrency ?? 1))}/s`,
|
||||
determination: 'fail',
|
||||
severity: 'high',
|
||||
detail: `Above the ${fmt(budget.maxBytesPerSecond)}/s ceiling${(budget.concurrency ?? 1) > 1 ? ` at concurrency ${budget.concurrency}` : ''}. The same bytes spread over a longer window would pass — the total is not the problem, the shape is.`,
|
||||
evidence: [stepEvidence(s)],
|
||||
}
|
||||
}
|
||||
|
||||
if (rate === null && budget.maxBytesPerSecond) {
|
||||
return {
|
||||
id,
|
||||
summary: `${s.name} has no duration, so its peak rate cannot be checked`,
|
||||
determination: 'not-assessed',
|
||||
severity: 'medium',
|
||||
detail: 'A rate ceiling is declared and this step cannot be tested against it.',
|
||||
evidence: [stepEvidence(s)],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
summary: `${s.name} — ${fmt(worstCaseBytes(s)!)} to ${s.destination}`,
|
||||
determination: 'pass',
|
||||
severity: 'info',
|
||||
evidence: [stepEvidence(s)],
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The total, computed over WORST-CASE bytes.
|
||||
*
|
||||
* Reported as its own finding, and reported as a lower bound when any step's
|
||||
* volume is unknown — a total assembled from partial measurements presented
|
||||
* as a total is the way a budget check reassures without checking.
|
||||
*/
|
||||
function totalFinding(steps: DeployStep[], budget: SignatureBudget): Finding | null {
|
||||
if (!budget.maxTotalBytes) return null
|
||||
|
||||
const known = steps.filter(s => s.bytes !== null)
|
||||
const unknown = steps.length - known.length
|
||||
const total = known.reduce((n, s) => n + worstCaseBytes(s)!, 0)
|
||||
const over = total > budget.maxTotalBytes
|
||||
|
||||
if (unknown > 0 && !over) {
|
||||
return {
|
||||
id: 'total',
|
||||
summary: `${fmt(total)} of ${fmt(budget.maxTotalBytes)} accounted for, with ${unknown} step(s) unmeasured`,
|
||||
determination: 'not-assessed',
|
||||
severity: 'high',
|
||||
detail: 'This is a lower bound, not a total. The unmeasured steps could put it over, and a partial sum presented as a total is how a budget check reassures without checking.',
|
||||
evidence: [evidence.measurement('worst-case egress', total, known.length, { unit: ' B' })],
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'total',
|
||||
summary: over
|
||||
? `${fmt(total)} exceeds the ${fmt(budget.maxTotalBytes)} budget`
|
||||
: `${fmt(total)} within the ${fmt(budget.maxTotalBytes)} budget`,
|
||||
determination: over ? 'fail' : 'pass',
|
||||
severity: over ? 'critical' : 'info',
|
||||
detail: over
|
||||
? 'Computed over worst-case bytes: every retry counts. A step budgeted at one attempt is budgeted at its best case.'
|
||||
: undefined,
|
||||
evidence: [evidence.measurement('worst-case egress', total, known.length, { unit: ' B' })],
|
||||
}
|
||||
}
|
||||
|
||||
/** Check a deployment plan against a signature budget. */
|
||||
export function checkPlan(steps: DeployStep[], budget: SignatureBudget = {}): CapabilityReport {
|
||||
const list = steps ?? []
|
||||
|
||||
if (list.length === 0) {
|
||||
return {
|
||||
capability: 'QuietDeploy',
|
||||
scope: 'no steps described',
|
||||
examined: 0,
|
||||
findings: [],
|
||||
notes: ['No deploy steps were described, so no signature was estimated. An unenumerated step still moves bytes.'],
|
||||
}
|
||||
}
|
||||
|
||||
const total = totalFinding(list, budget)
|
||||
const destinations = [...new Set(list.map(s => s.destination))].sort()
|
||||
|
||||
return {
|
||||
capability: 'QuietDeploy',
|
||||
scope: `${list.length} step(s) across ${destinations.length} destination(s)`,
|
||||
examined: list.length,
|
||||
findings: [...(total ? [total] : []), ...list.map(s => stepFinding(s, budget))],
|
||||
notes: [
|
||||
`destinations: ${destinations.join(', ')}`,
|
||||
...(budget.allowedDestinations?.length
|
||||
? []
|
||||
: ['No destination allowlist was supplied, so no step could be checked against one.']),
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
/** Render a signature check for a terminal, CI log or review. */
|
||||
export function formatPlan(
|
||||
steps: DeployStep[],
|
||||
budget: SignatureBudget = {},
|
||||
opts: FormatOptions = {},
|
||||
): string {
|
||||
return renderReport(checkPlan(steps, budget), opts)
|
||||
}
|
||||
|
||||
export { fmt as formatBytes }
|
||||
@@ -0,0 +1,125 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { overall, validateReport } from '@extant2000/evidence-record'
|
||||
import {
|
||||
checkPlan,
|
||||
formatPlan,
|
||||
peakRate,
|
||||
worstCaseBytes,
|
||||
type DeployStep,
|
||||
type SignatureBudget,
|
||||
} from '../src/index.js'
|
||||
|
||||
const step = (p: Partial<DeployStep> = {}): DeployStep => ({
|
||||
name: 'pull-base-image',
|
||||
destination: 'registry.gitlab.com',
|
||||
bytes: 200_000_000,
|
||||
durationSeconds: 60,
|
||||
where: '.gitlab-ci.yml',
|
||||
...p,
|
||||
})
|
||||
|
||||
describe('retries are cost, not a footnote', () => {
|
||||
it('counts every attempt in the worst case', () => {
|
||||
// A step budgeted at one attempt is budgeted at its best case.
|
||||
expect(worstCaseBytes(step({ bytes: 100, retries: 3 }))).toBe(300)
|
||||
expect(worstCaseBytes(step({ bytes: 100 }))).toBe(100)
|
||||
})
|
||||
|
||||
it('a plan that fits on first attempt and not with retries fails', () => {
|
||||
const budget: SignatureBudget = { maxTotalBytes: 250_000_000 }
|
||||
expect(overall(checkPlan([step({ retries: 1 })], budget))).toBe('pass')
|
||||
const r = checkPlan([step({ retries: 2 })], budget)
|
||||
expect(r.findings.find(f => f.id === 'total')!.determination).toBe('fail')
|
||||
expect(r.findings.find(f => f.id === 'total')!.detail).toContain('every retry counts')
|
||||
})
|
||||
})
|
||||
|
||||
describe('an unexpected destination is a different kind of problem', () => {
|
||||
const budget: SignatureBudget = { allowedDestinations: ['registry.gitlab.com'] }
|
||||
|
||||
it('fails a step reaching outside the declared set', () => {
|
||||
const r = checkPlan([step({ name: 'fetch-tool', destination: 'cdn.example.net', bytes: 1024 })], budget)
|
||||
expect(r.findings.find(f => f.id === 'fetch-tool')!.determination).toBe('fail')
|
||||
expect(r.findings.find(f => f.id === 'fetch-tool')!.severity).toBe('critical')
|
||||
})
|
||||
|
||||
it('stays critical however small the transfer', () => {
|
||||
// Volume is irrelevant: an unexpected host is supply chain or leak.
|
||||
const r = checkPlan([step({ name: 'ping', destination: 'evil.example', bytes: 1 })], budget)
|
||||
expect(r.findings.find(f => f.id === 'ping')!.severity).toBe('critical')
|
||||
expect(formatPlan([step({ name: 'ping', destination: 'evil.example', bytes: 1 })], budget))
|
||||
.toContain('Volume is irrelevant here')
|
||||
})
|
||||
|
||||
it('says so when no allowlist was supplied', () => {
|
||||
expect(formatPlan([step()], {})).toContain('no step could be checked against one')
|
||||
})
|
||||
|
||||
it('passes a step inside the declared set', () => {
|
||||
expect(overall(checkPlan([step()], budget))).toBe('pass')
|
||||
})
|
||||
})
|
||||
|
||||
describe('shape, not just total', () => {
|
||||
it('fails a burst that fits the total budget', () => {
|
||||
// 200 MB over 60s = 3.3 MB/s. The same bytes over an hour would pass.
|
||||
const r = checkPlan([step()], { maxTotalBytes: 1e9, maxBytesPerSecond: 1_000_000 })
|
||||
const f = r.findings.find(x => x.id === 'pull-base-image')!
|
||||
expect(f.determination).toBe('fail')
|
||||
expect(f.detail).toContain('the total is not the problem, the shape is')
|
||||
})
|
||||
|
||||
it('multiplies the peak by concurrency', () => {
|
||||
const budget: SignatureBudget = { maxBytesPerSecond: 5_000_000, concurrency: 4 }
|
||||
// 3.3 MB/s alone passes; x4 concurrent does not.
|
||||
expect(overall(checkPlan([step()], { maxBytesPerSecond: 5_000_000 }))).toBe('pass')
|
||||
expect(overall(checkPlan([step()], budget))).toBe('fail')
|
||||
})
|
||||
|
||||
it('cannot check a rate without a duration, and says so', () => {
|
||||
const r = checkPlan([step({ durationSeconds: undefined })], { maxBytesPerSecond: 1_000_000 })
|
||||
expect(r.findings.find(f => f.id === 'pull-base-image')!.determination).toBe('not-assessed')
|
||||
expect(peakRate(step({ durationSeconds: undefined }))).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('an unmeasured transfer is not a small one', () => {
|
||||
it('reports an unknown volume as not-assessed, never zero', () => {
|
||||
const r = checkPlan([step({ bytes: null })], { maxTotalBytes: 1e9 })
|
||||
const f = r.findings.find(x => x.id === 'pull-base-image')!
|
||||
expect(f.determination).toBe('not-assessed')
|
||||
expect(f.detail).toContain('An unmeasured transfer is not a small one')
|
||||
})
|
||||
|
||||
it('reports the total as a LOWER BOUND when any step is unmeasured', () => {
|
||||
// A partial sum presented as a total is how a budget check reassures
|
||||
// without checking.
|
||||
const r = checkPlan([step({ bytes: 1000 }), step({ name: 'unknown-pull', bytes: null })],
|
||||
{ maxTotalBytes: 1e9 })
|
||||
const total = r.findings.find(f => f.id === 'total')!
|
||||
expect(total.determination).toBe('not-assessed')
|
||||
expect(total.detail).toContain('lower bound, not a total')
|
||||
})
|
||||
|
||||
it('still fails when the KNOWN bytes alone exceed the budget', () => {
|
||||
// Unmeasured steps cannot rescue a plan that is already over.
|
||||
const r = checkPlan([step({ bytes: 2e9 }), step({ name: 'x', bytes: null })], { maxTotalBytes: 1e9 })
|
||||
expect(r.findings.find(f => f.id === 'total')!.determination).toBe('fail')
|
||||
})
|
||||
})
|
||||
|
||||
describe('conformance with the evidence-record standard', () => {
|
||||
it('an empty plan is not-assessed, and says what empty means', () => {
|
||||
expect(overall(checkPlan([]))).toBe('not-assessed')
|
||||
expect(formatPlan([])).toContain('An unenumerated step still moves bytes')
|
||||
})
|
||||
|
||||
it('every conclusion carries a citation', () => {
|
||||
expect(validateReport(checkPlan([step(), step({ name: 'b', destination: 'x', bytes: 5 })],
|
||||
{ maxTotalBytes: 1e9, allowedDestinations: ['registry.gitlab.com'] }))).toEqual([])
|
||||
})
|
||||
|
||||
it('cites the file each step is defined in', () => {
|
||||
expect(formatPlan([step()], {}, { evidence: 'full' })).toContain('.gitlab-ci.yml')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"outDir": "dist-cjs",
|
||||
"declaration": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022", "module": "ES2022", "moduleResolution": "bundler",
|
||||
"declaration": true, "outDir": "dist", "rootDir": "src",
|
||||
"strict": true, "skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
Reference in New Issue
Block a user