mirror of https://github.com/garrytan/gstack.git
feat(ci): supply-chain hygiene — secret gate on every PR diff, dependency review, OSV, dependabot, evidence-bar PR template
The repo owned a redaction engine and had zero CI-side secret scanning. quality-gate.yml now pipes every PR diff's ADDED lines through our own bin/gstack-redact (gate-secret-scan.mjs, taken from the fork — it dogfoods the engine): HIGH findings fail the check, MEDIUM prints an advisory count only (no human in CI to confirm), planted-bug fixtures excluded by pathspec. Live-verified both directions: PEM key fails, clean diff and MEDIUM shapes pass; ShellCheck (errors) covers the setup/build shell boundary and passes today; bun audit gates critical advisories. Trigger is pull_request, never pull_request_target. dependency-review.yml adopts the hardened never-merged prior-art branch (fail-on-severity high, workflow paths watched, tight perms) — verify the dependency graph parses bun.lock with a canary bump before trusting the gate. dependabot: weekly, grouped per ecosystem, capped PR counts; and evals.yml image build/push now skips dependabot actors, whose read-only GITHUB_TOKEN made every lockfile bump a permanently red check. OSV scans weekly with a reasoned ignore file. All new workflow actions SHA-pinned. Scorecard deliberately not taken (no consumer for the score). The PR template front-loads the evidence bar (live proof, liveness screenshot, no-ETHOS/voice-changes checklist); the unenforced DCO line is dropped. bin/gstack-verify-gate ships OPT-IN (never registered by ./setup — a Stop hook running the project's verify command after every turn is the user's call), with the fork's tests adapted to pin exactly that. Ported from time-attack/gstack (GStack 2) + our own prior-art branch. Co-authored-by: Sina Matian <sina@time-attack.dev> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3aba218303
commit
8c75496391
|
|
@ -0,0 +1,44 @@
|
|||
<!--
|
||||
gstack is AI-coded and proud of it. The bar is EVIDENCE OF REAL USE, not lines
|
||||
of code. A PR with no proof behind it gets closed, no matter how clean it looks.
|
||||
Fill every section below. See CONTRIBUTING.md → "The evidence bar".
|
||||
-->
|
||||
|
||||
## Why (in your own words)
|
||||
|
||||
<!-- One paragraph: what breaks for a user today, and what this change does about
|
||||
it. Not a restatement of the diff. -->
|
||||
|
||||
## Live evidence
|
||||
|
||||
<!-- REQUIRED. Paste the command(s) you ran and their real output — before and
|
||||
after. For a bug: the reproduction, failing then fixed. For a skill change: the
|
||||
actual transcript / `claude -p` output. For anything visual: before/after
|
||||
screenshots. "bun test passes" alone is not enough — show the behavior you
|
||||
changed. -->
|
||||
|
||||
```
|
||||
# what you ran + what it produced
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
- **Changed:**
|
||||
- **Verified live by:**
|
||||
- **Did NOT test:**
|
||||
|
||||
## Liveness proof (required)
|
||||
|
||||
<!-- Attach a screenshot of your own machine with the text `GSTACK PR` typed LIVE
|
||||
into a real surface — terminal prompt, a shell command, your browser
|
||||
address/search bar, an editor buffer. It must be TYPED INTO A LIVE UI, not drawn,
|
||||
overlaid, or edited onto the image. A painted-on `GSTACK PR` is an automatic
|
||||
close. This confirms a human opened this PR. -->
|
||||
|
||||
## Checklist
|
||||
|
||||
- [ ] Liveness screenshot attached: `GSTACK PR` typed live into a real surface (not edited onto the image)
|
||||
- [ ] This is not a generated-file-only diff (I edited the source/template and regenerated)
|
||||
- [ ] No ETHOS.md edits, and no changes to voice / founder perspective / YC references
|
||||
- [ ] New public command / external service / host adapter has an accepted issue linked (or N/A)
|
||||
- [ ] Linked issue or reproduction: #
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "bun"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
groups:
|
||||
production-dependencies:
|
||||
dependency-type: "production"
|
||||
development-dependencies:
|
||||
dependency-type: "development"
|
||||
open-pull-requests-limit: 5
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
day: "monday"
|
||||
groups:
|
||||
github-actions:
|
||||
patterns:
|
||||
- "*"
|
||||
open-pull-requests-limit: 2
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
#!/usr/bin/env node
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const child = spawn("bun", [
|
||||
"bin/gstack-redact",
|
||||
"--repo-visibility", "public",
|
||||
"--json",
|
||||
"--max-bytes", "16000000",
|
||||
], { shell: false, windowsHide: true, stdio: ["pipe", "pipe", "inherit"] });
|
||||
let diff = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => { diff += chunk; });
|
||||
process.stdin.once("end", () => {
|
||||
const additions = diff
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith("+") && !line.startsWith("+++"))
|
||||
.map((line) => line.slice(1))
|
||||
.join("\n");
|
||||
child.stdin.end(additions);
|
||||
});
|
||||
let stdout = "";
|
||||
child.stdout.setEncoding("utf8");
|
||||
child.stdout.on("data", (chunk) => { stdout += chunk; });
|
||||
child.once("error", (error) => { throw error; });
|
||||
child.once("close", (code) => {
|
||||
const report = JSON.parse(stdout);
|
||||
const high = Number(report.counts?.HIGH ?? 0);
|
||||
const medium = Number(report.counts?.MEDIUM ?? 0);
|
||||
console.log(`credential scan: ${high} high, ${medium} advisory`);
|
||||
process.exitCode = high > 0 || report.oversize || ![0, 2, 3].includes(code) ? 1 : 0;
|
||||
});
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
name: Dependency Review
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- 'package.json'
|
||||
- 'bun.lock'
|
||||
- '**/package.json'
|
||||
- '**/bun.lock'
|
||||
- '.github/workflows/**'
|
||||
|
||||
concurrency:
|
||||
group: dependency-review-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
dependency-review:
|
||||
runs-on: ubicloud-standard-8
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/dependency-review-action@v4
|
||||
with:
|
||||
fail-on-severity: high
|
||||
fail-on-scopes: runtime, development
|
||||
comment-summary-in-pr: on-failure
|
||||
|
|
@ -15,6 +15,12 @@ env:
|
|||
jobs:
|
||||
# Build Docker image with pre-baked toolchain (cached — only rebuilds on Dockerfile/lockfile change)
|
||||
build-image:
|
||||
# Dependabot-triggered pull_request runs get a read-only GITHUB_TOKEN, so
|
||||
# a lockfile bump = new hash = failed ghcr push = permanently red check
|
||||
# (EV6, fork port wave 2). Skip the build for dependabot; the evals job's
|
||||
# needs-chain tolerates it because no eval test selects on a lockfile-only
|
||||
# diff — a maintainer's next push rebuilds the image with real perms.
|
||||
if: github.actor != 'dependabot[bot]'
|
||||
runs-on: ubicloud-standard-8
|
||||
permissions:
|
||||
contents: read
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
name: OSV Scanner
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '23 7 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: osv-scanner
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
scan:
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@3adb4b14a2b0623876d18d863a498b785fb3752d # v2.3.8
|
||||
with:
|
||||
scan-args: |-
|
||||
--include-git-root
|
||||
--recursive
|
||||
./
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
# Quality gate (fork port wave 2, adapted from time-attack/gstack GStack 2).
|
||||
#
|
||||
# Three generic hygiene checks the repo previously had nowhere in CI:
|
||||
# 1. Credential scan of the PR diff's ADDED lines through our own
|
||||
# bin/gstack-redact (HIGH fails the check; MEDIUM is an advisory count —
|
||||
# there is no human in CI to confirm, so it never fails here).
|
||||
# 2. bun audit at critical severity.
|
||||
# 3. ShellCheck (errors only) on the setup/build shell boundary.
|
||||
#
|
||||
# Trigger is `pull_request`, NEVER `pull_request_target`: fork PRs must not
|
||||
# get secret-bearing contexts. Diff excludes cover the planted-bug fixtures
|
||||
# and eval baselines that intentionally contain credential-shaped strings.
|
||||
name: Quality gate
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches: [main]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: quality-gate-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
quality:
|
||||
runs-on: ubicloud-standard-8
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
bun-version: latest
|
||||
|
||||
- name: Install frozen dependencies
|
||||
run: bun install --frozen-lockfile --ignore-scripts
|
||||
|
||||
- name: Scan changed text for credentials (added lines, own redact engine)
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
|
||||
BASE_SHA=$(git rev-parse HEAD^)
|
||||
fi
|
||||
git diff --unified=0 --no-color "$BASE_SHA" "$HEAD_SHA" -- \
|
||||
. \
|
||||
':(exclude)test/fixtures/**' \
|
||||
':(exclude)browse/test/fixtures/**' \
|
||||
':(exclude)docs/evals/**' \
|
||||
':(exclude)test/helpers/security-bench*' \
|
||||
| node .github/scripts/gate-secret-scan.mjs
|
||||
|
||||
- name: Gate critical dependency advisories
|
||||
run: bun audit --audit-level=critical
|
||||
|
||||
- name: Install ShellCheck
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck
|
||||
shellcheck --version
|
||||
|
||||
- name: ShellCheck setup and build boundaries
|
||||
run: >-
|
||||
shellcheck --severity=error
|
||||
setup
|
||||
scripts/build.sh
|
||||
scripts/build-app.sh
|
||||
scripts/write-version-files.sh
|
||||
browse/scripts/build-node-server.sh
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
# OSV-Scanner configuration.
|
||||
# Direct/transitive dependency versions are pinned to their fixed releases via
|
||||
# the `overrides` block in package.json; this file only records advisories we
|
||||
# have assessed as not-reachable or not-fixable without disproportionate risk.
|
||||
|
||||
[[IgnoredVulns]]
|
||||
id = "GHSA-frvp-7c67-39w9"
|
||||
# @hono/node-server 1.19.x. Reachable only through @modelcontextprotocol/sdk,
|
||||
# which is an unused transitive dependency (no source file imports it) and never
|
||||
# starts a Hono HTTP server, so the advisory's request path is not exercised.
|
||||
# The only fix is @hono/node-server 2.0.5, a major bump the MCP SDK pins against
|
||||
# (^1.19.9); forcing it via override risks breaking the SDK at runtime for a
|
||||
# vulnerability we do not expose. Re-evaluate if the MCP SDK becomes a direct,
|
||||
# server-hosting dependency.
|
||||
reason = "Unreachable transitive (unused @modelcontextprotocol/sdk); fix requires a risky major override on a pinned peer dep."
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
#!/usr/bin/env bash
|
||||
# gstack-verify-gate — Stop hook. Blocks the turn from ending until the
|
||||
# project's declared verification command passes.
|
||||
#
|
||||
# Declare the command on one line in the project's CLAUDE.md:
|
||||
# <!-- gstack:verify: bun test -->
|
||||
#
|
||||
# Read-or-ask: gstack never invents this command. No declaration, no gate.
|
||||
# Fails open on every absence (no CLAUDE.md, no declaration, empty value).
|
||||
#
|
||||
# Exit 0 = allow the turn to end, one-line reason on stdout.
|
||||
# Exit 2 = block, Claude Code feeds stderr back to the agent.
|
||||
#
|
||||
# Remove with: gstack-settings-hook remove-source --source verify-gate
|
||||
set -uo pipefail
|
||||
|
||||
INPUT=""
|
||||
[ -t 0 ] || INPUT="$(cat)"
|
||||
|
||||
# Claude Code re-runs Stop hooks after a block. Never gate the same turn twice.
|
||||
if printf '%s' "$INPUT" | grep -q '"stop_hook_active"[[:space:]]*:[[:space:]]*true'; then
|
||||
echo "verify-gate: gate already ran this turn, allowing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
ROOT="${CLAUDE_PROJECT_DIR:-$PWD}"
|
||||
while [ ! -f "$ROOT/CLAUDE.md" ] && [ "$ROOT" != "/" ]; do
|
||||
ROOT="$(dirname "$ROOT")"
|
||||
done
|
||||
|
||||
if [ ! -f "$ROOT/CLAUDE.md" ]; then
|
||||
echo "verify-gate: no CLAUDE.md above $PWD, no check declared, allowing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
CMD="$(sed -n 's/^[[:space:]]*\(<!--[[:space:]]*\)\{0,1\}gstack:verify:[[:space:]]*\(.*\)$/\2/p' "$ROOT/CLAUDE.md" | head -1)"
|
||||
CMD="${CMD%%-->*}"
|
||||
CMD="$(printf '%s' "$CMD" | tr -d '`' | sed 's/[[:space:]]*$//')"
|
||||
|
||||
if [ -z "$CMD" ]; then
|
||||
echo "verify-gate: $ROOT/CLAUDE.md declares no 'gstack:verify:' command, allowing."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
OUT="$(cd "$ROOT" && eval "$CMD" 2>&1)"
|
||||
STATUS=$?
|
||||
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
echo "verify-gate: declared check passed ($CMD)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "verify-gate: declared check FAILED with exit $STATUS: $CMD" >&2
|
||||
printf '%s\n' "$OUT" | tail -20 >&2
|
||||
echo "Fix the failure, or drop the gstack:verify line from $ROOT/CLAUDE.md." >&2
|
||||
exit 2
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
/**
|
||||
* CI secret gate contract (R4/R9, fork port wave 2).
|
||||
*
|
||||
* .github/scripts/gate-secret-scan.mjs pipes a unified diff's ADDED lines
|
||||
* into bin/gstack-redact and enforces: HIGH fails (exit 1), MEDIUM is an
|
||||
* advisory count only (no human in CI to confirm, so it must never fail
|
||||
* the check), clean passes. The workflow-level pathspec excludes keep the
|
||||
* planted-bug fixtures out of the diff entirely; this pins the script's
|
||||
* own exit contract with live subprocess runs.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from "bun:test";
|
||||
import { spawnSync } from "child_process";
|
||||
import { join } from "path";
|
||||
|
||||
const ROOT = join(import.meta.dir, "..");
|
||||
const SCRIPT = join(ROOT, ".github", "scripts", "gate-secret-scan.mjs");
|
||||
|
||||
function scan(diff: string): { code: number; out: string } {
|
||||
const res = spawnSync("node", [SCRIPT], {
|
||||
cwd: ROOT,
|
||||
input: diff,
|
||||
encoding: "utf-8",
|
||||
timeout: 60_000,
|
||||
});
|
||||
return { code: res.status ?? -1, out: `${res.stdout}${res.stderr}` };
|
||||
}
|
||||
|
||||
describe("gate-secret-scan.mjs exit contract", () => {
|
||||
test("clean added lines pass", () => {
|
||||
const r = scan("+const x = 1;\n+++ b/file.ts\n+// harmless\n");
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.out).toContain("0 high");
|
||||
});
|
||||
|
||||
test("a HIGH credential in an added line fails the gate", () => {
|
||||
const r = scan(
|
||||
"+-----BEGIN RSA PRIVATE KEY-----\n+MIIEowIBAAKCAQEA\n+-----END RSA PRIVATE KEY-----\n",
|
||||
);
|
||||
expect(r.code).toBe(1);
|
||||
expect(r.out).toContain("1 high");
|
||||
});
|
||||
|
||||
test("removed lines and context are ignored — only additions are scanned", () => {
|
||||
const r = scan(
|
||||
"------BEGIN RSA PRIVATE KEY-----\n-MIIEowIBAAKCAQEA\n-----END RSA PRIVATE KEY-----\n+just an addition\n",
|
||||
);
|
||||
expect(r.code).toBe(0);
|
||||
});
|
||||
|
||||
test("MEDIUM findings are advisory only — never fail CI", () => {
|
||||
// A Stripe publishable-key shape sits at MEDIUM in the taxonomy
|
||||
// (context-variable; a human confirms interactively, CI cannot).
|
||||
const r = scan(`+const key = "pk_live_${"a".repeat(24)}";\n`);
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.out).toMatch(/\d+ advisory/);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* gstack-verify-gate — Stop-hook enforcement tier.
|
||||
*
|
||||
* Pins the three behaviours the gate exists for:
|
||||
* block — declared check fails, exit 2, turn cannot end.
|
||||
* allow — declared check passes, exit 0.
|
||||
* fail open — nothing declared, exit 0. Absence never blocks.
|
||||
*
|
||||
* Plus the two safety branches: the Stop re-entry guard, and the static
|
||||
* opt-in contract (our adaptation): ./setup never registers the gate; the
|
||||
* settings-hook helper.
|
||||
*/
|
||||
|
||||
import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import { spawnSync } from 'child_process';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const GATE = path.join(ROOT, 'bin', 'gstack-verify-gate');
|
||||
|
||||
let dir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gstack-verify-gate-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
/** Declare a verification command in the project's CLAUDE.md. */
|
||||
function declareCheck(command: string): void {
|
||||
fs.writeFileSync(path.join(dir, 'CLAUDE.md'), `# Fixture\n\n<!-- gstack:verify: ${command} -->\n`);
|
||||
}
|
||||
|
||||
/** Write the check script the declaration points at. */
|
||||
function check(exitCode: number, message: string): void {
|
||||
const script = path.join(dir, 'check.sh');
|
||||
fs.writeFileSync(script, `#!/bin/sh\necho "${message}"\nexit ${exitCode}\n`);
|
||||
fs.chmodSync(script, 0o755);
|
||||
}
|
||||
|
||||
function runGate(stopHookActive = false): { code: number; stdout: string; stderr: string } {
|
||||
const r = spawnSync(GATE, {
|
||||
cwd: dir,
|
||||
input: JSON.stringify({ stop_hook_active: stopHookActive }),
|
||||
encoding: 'utf-8',
|
||||
timeout: 15000,
|
||||
env: { ...process.env, CLAUDE_PROJECT_DIR: dir },
|
||||
});
|
||||
return { code: r.status ?? 1, stdout: r.stdout || '', stderr: r.stderr || '' };
|
||||
}
|
||||
|
||||
describe('gstack-verify-gate', () => {
|
||||
test('blocks the turn when the declared check fails', () => {
|
||||
declareCheck('./check.sh');
|
||||
check(1, 'totals mismatch');
|
||||
|
||||
const r = runGate();
|
||||
|
||||
expect(r.code).toBe(2);
|
||||
expect(r.stderr).toContain('FAILED');
|
||||
expect(r.stderr).toContain('totals mismatch');
|
||||
});
|
||||
|
||||
test('allows the turn when the declared check passes', () => {
|
||||
declareCheck('./check.sh');
|
||||
check(0, 'all good');
|
||||
|
||||
const r = runGate();
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain('passed');
|
||||
});
|
||||
|
||||
test('fails open when CLAUDE.md declares no check', () => {
|
||||
fs.writeFileSync(path.join(dir, 'CLAUDE.md'), '# Fixture\n\nNothing declared here.\n');
|
||||
|
||||
const r = runGate();
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain("declares no 'gstack:verify:' command");
|
||||
});
|
||||
|
||||
test('fails open when there is no CLAUDE.md at all', () => {
|
||||
const r = runGate();
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain('no CLAUDE.md');
|
||||
});
|
||||
|
||||
test('never invents a command: an empty declaration fails open', () => {
|
||||
fs.writeFileSync(path.join(dir, 'CLAUDE.md'), '<!-- gstack:verify: -->\n');
|
||||
|
||||
const r = runGate();
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain("declares no 'gstack:verify:' command");
|
||||
});
|
||||
|
||||
test('re-entry guard: a failing check does not block twice in one turn', () => {
|
||||
declareCheck('./check.sh');
|
||||
check(1, 'still failing');
|
||||
|
||||
const r = runGate(true);
|
||||
|
||||
expect(r.code).toBe(0);
|
||||
expect(r.stdout).toContain('already ran');
|
||||
});
|
||||
});
|
||||
|
||||
describe('opt-in contract (adapted from the fork: NOT registered by default)', () => {
|
||||
const setup = fs.readFileSync(path.join(ROOT, 'setup'), 'utf-8');
|
||||
const gate = fs.readFileSync(GATE, 'utf-8');
|
||||
|
||||
test('./setup does NOT register the gate — a Stop hook running the verify command after every turn is opt-in', () => {
|
||||
expect(setup).not.toContain('verify-gate');
|
||||
});
|
||||
|
||||
test('the bin documents its own registration and removal commands', () => {
|
||||
expect(gate).toContain('remove-source --source verify-gate');
|
||||
expect(gate).toContain('gstack:verify:');
|
||||
});
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue