@1aboveio/skills 1.0.8 → 1.0.10
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/.codex-plugin/plugin.json +13 -0
- package/.codex-plugin/resources.json +1827 -0
- package/README.md +22 -0
- package/package.json +9 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/airflow/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/release-check.mjs +3 -1
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/app-debug/release-check.mjs +3 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/better-auth/release-check.mjs +3 -1
- package/skills/app-dev/data-catalog/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/release-check.mjs +3 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/data-quality/release-check.mjs +3 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/release-check.mjs +3 -1
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/monorepo/release-check.mjs +3 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/release-check.mjs +3 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/release-check.mjs +3 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/pyspark/release-check.mjs +3 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/python-backend/release-check.mjs +3 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/shadcn/release-check.mjs +3 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/release-check.mjs +3 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-build/release-check.mjs +3 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/release-check.mjs +3 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/release-check.mjs +3 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/docker/release-check.mjs +3 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-actions/release-check.mjs +3 -1
- package/skills/cicd/github-flow/SKILL.md +1 -1
- package/skills/cicd/github-flow/release-check.mjs +3 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/google-cloud/release-check.mjs +3 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/mergify/release-check.mjs +3 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/cicd/podman/release-check.mjs +3 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-review/release-check.mjs +3 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/article-to-image/release-check.mjs +3 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/flowchart-svg/release-check.mjs +3 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/image-gen/release-check.mjs +3 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/mermaid/release-check.mjs +3 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/release-check.mjs +3 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-publisher/release-check.mjs +3 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/wechat-search/release-check.mjs +3 -1
- package/skills/content/xai-search/SKILL.md +88 -13
- package/skills/content/xai-search/release-check.mjs +3 -1
- package/skills/content/xai-search/scripts/xai_search.sh +125 -149
- package/skills/core/distill-writing/SKILL.md +1 -1
- package/skills/core/distill-writing/release-check.mjs +3 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/doc-to-md/release-check.mjs +3 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/editorial-review/release-check.mjs +3 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/release-check.mjs +3 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/plain-language/release-check.mjs +3 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/polish-cn/release-check.mjs +3 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/reg-parser/release-check.mjs +3 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/release-check.mjs +3 -1
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-doc/release-check.mjs +3 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/better-review/release-check.mjs +3 -1
- package/skills/engineering/contributing/SKILL.md +1 -1
- package/skills/engineering/contributing/release-check.mjs +3 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/e2e-test/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/engineering-runtime/release-check.mjs +3 -1
- package/skills/engineering/engineering-runtime/scripts/installation-resolver.mjs +191 -0
- package/skills/engineering/engineering-runtime/scripts/workflow-coherence.mjs +51 -10
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/ensure-coverage/release-check.mjs +3 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/golden-path/release-check.mjs +3 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/preflight.md +7 -0
- package/skills/engineering/resolve-issues/references/shared-modules.md +27 -0
- package/skills/engineering/resolve-issues/references/task-plan.md +5 -1
- package/skills/engineering/resolve-issues/release-check.mjs +3 -1
- package/skills/engineering/resolve-issues/scripts/active-installation.mjs +32 -0
- package/skills/engineering/resolve-issues/scripts/install-hooks.mjs +3 -0
- package/skills/engineering/resolve-issues/scripts/rehydrate.mjs +17 -1
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/resolve-release/release-check.mjs +3 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-issues/release-check.mjs +3 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/rush-release/release-check.mjs +3 -1
- package/skills/engineering/test-quality/SKILL.md +1 -1
- package/skills/engineering/test-quality/release-check.mjs +3 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/compliance-audit/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/release-check.mjs +3 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/release-check.mjs +3 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/release-check.mjs +3 -1
- package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/release-check.mjs +3 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/release-check.mjs +3 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/release-check.mjs +3 -1
- package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +0 -436
- package/skills/app-dev/data-catalog/scripts/data-catalog.test.mjs +0 -36
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +0 -12
- package/skills/cicd/github-actions/scripts/ci-run-audit.test.mjs +0 -637
- package/skills/cicd/github-flow/scripts/flow.test.mjs +0 -114
- package/skills/cicd/github-flow/scripts/persist-conventions.test.mjs +0 -108
- package/skills/cicd/mergify/scripts/audit.test.mjs +0 -201
- package/skills/cicd/mergify/scripts/entrypoints.test.mjs +0 -106
- package/skills/cicd/mergify/scripts/pr-620-mergify-comments.fixture.json +0 -30
- package/skills/cicd/mergify/scripts/stack-push.test.mjs +0 -233
- package/skills/cicd/mergify/scripts/watch-pr-delivery.test.mjs +0 -1826
- package/skills/content/image-gen/scripts/credentials.test.mjs +0 -212
- package/skills/content/image-gen/scripts/options.test.mjs +0 -245
- package/skills/content/xai-search/tests/smoke_test.sh +0 -36
- package/skills/core/polish-cn/scripts/polish-cn.test.mjs +0 -244
- package/skills/core/reg-parser/scripts/reg-parser.test.mjs +0 -165
- package/skills/engineering/better-review/scripts/pr-context.test.mjs +0 -168
- package/skills/engineering/better-review/scripts/rereview-scope.test.mjs +0 -587
- package/skills/engineering/better-review/scripts/scan-diff.test.mjs +0 -414
- package/skills/engineering/better-review/scripts/vendor-sync.test.mjs +0 -96
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +0 -118
- package/skills/engineering/e2e-test/scripts/presentation-checks.test.mjs +0 -106
- package/skills/engineering/e2e-test/scripts/presentation-perceivability.test.mjs +0 -117
- package/skills/engineering/e2e-test/scripts/presentation-reachability.test.mjs +0 -69
- package/skills/engineering/e2e-test/scripts/presentation-render-health.test.mjs +0 -98
- package/skills/engineering/e2e-test/scripts/presentation-sweep.test.mjs +0 -71
- package/skills/engineering/e2e-test/scripts/presentation-temporal.test.mjs +0 -59
- package/skills/engineering/e2e-test/scripts/project-route-manifest.test.mjs +0 -76
- package/skills/engineering/engineering-runtime/engineering-runtime.test.mjs +0 -68
- package/skills/engineering/engineering-runtime/scripts/invocation-policy.test.mjs +0 -95
- package/skills/engineering/engineering-runtime/scripts/main-module.test.mjs +0 -288
- package/skills/engineering/engineering-runtime/scripts/required-check-plan.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/adapters/nextjs-inventory.test.mjs +0 -96
- package/skills/engineering/ensure-coverage/scripts/check-quarantine-expiry.test.mjs +0 -47
- package/skills/engineering/ensure-coverage/scripts/ci-audit.test.mjs +0 -86
- package/skills/engineering/ensure-coverage/scripts/coverage-ledger.test.mjs +0 -447
- package/skills/engineering/ensure-coverage/scripts/design-parity.test.mjs +0 -552
- package/skills/engineering/ensure-coverage/scripts/lint-tests.test.mjs +0 -289
- package/skills/engineering/ensure-coverage/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/golden-path/scripts/golden-path.test.mjs +0 -1457
- package/skills/engineering/harness-runtime/discover-models.test.mjs +0 -363
- package/skills/engineering/harness-runtime/harness-runtime.test.mjs +0 -967
- package/skills/engineering/harness-runtime/model-catalog-seed.test.mjs +0 -829
- package/skills/engineering/harness-runtime/model-catalog.test.mjs +0 -366
- package/skills/engineering/resolve-issues/scripts/cleanup-runs.test.mjs +0 -280
- package/skills/engineering/resolve-issues/scripts/dropbox.test.mjs +0 -346
- package/skills/engineering/resolve-issues/scripts/findings-reuse.test.mjs +0 -88
- package/skills/engineering/resolve-issues/scripts/github-reviews.test.mjs +0 -264
- package/skills/engineering/resolve-issues/scripts/install-hooks.test.mjs +0 -79
- package/skills/engineering/resolve-issues/scripts/issue-tree.test.mjs +0 -107
- package/skills/engineering/resolve-issues/scripts/manual-invocation.test.mjs +0 -14
- package/skills/engineering/resolve-issues/scripts/next-actions-capacity.test.mjs +0 -149
- package/skills/engineering/resolve-issues/scripts/next-actions.test.mjs +0 -915
- package/skills/engineering/resolve-issues/scripts/plan-sync.test.mjs +0 -156
- package/skills/engineering/resolve-issues/scripts/preflight-models.test.mjs +0 -448
- package/skills/engineering/resolve-issues/scripts/ready-check.test.mjs +0 -217
- package/skills/engineering/resolve-issues/scripts/rehydrate.test.mjs +0 -91
- package/skills/engineering/resolve-issues/scripts/review-verdict.test.mjs +0 -1060
- package/skills/engineering/resolve-issues/scripts/size-check.test.mjs +0 -458
- package/skills/engineering/resolve-release/scripts/apply.test.mjs +0 -99
- package/skills/engineering/resolve-release/scripts/dark-flags.test.mjs +0 -100
- package/skills/engineering/resolve-release/scripts/flow.test.mjs +0 -340
- package/skills/engineering/resolve-release/scripts/green-head.test.mjs +0 -101
- package/skills/engineering/resolve-release/scripts/npm-version-absence.test.mjs +0 -426
- package/skills/engineering/resolve-release/scripts/plan-npm.test.mjs +0 -659
- package/skills/engineering/resolve-release/scripts/plan-toml-boundary.test.mjs +0 -317
- package/skills/engineering/resolve-release/scripts/plan-uv.test.mjs +0 -291
- package/skills/engineering/resolve-release/scripts/plan.test.mjs +0 -428
- package/skills/engineering/test-quality/scripts/lint-tests.test.mjs +0 -591
- package/skills/engineering/test-quality/scripts/mock-policy-config.test.mjs +0 -119
- package/skills/engineering/test-quality/scripts/persist-conventions.test.mjs +0 -104
- package/skills/risk-compliance/compliance-audit/scripts/compliance-audit.test.mjs +0 -14
- package/skills/risk-compliance/doc-authenticity/manual-invocation.test.mjs +0 -12
- package/skills/risk-compliance/doc-authenticity/scripts/validate_verdict.test.mjs +0 -212
- package/skills/risk-compliance/tld-detection/scripts/detect_ladders.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/scripts/detect_streams.test.mjs +0 -14
- package/skills/risk-compliance/tld-detection/tests/test_detect_ladders.py +0 -134
- package/skills/risk-compliance/tld-detection/tests/test_detect_streams.py +0 -520
|
@@ -1,1457 +0,0 @@
|
|
|
1
|
-
// Tests for the golden-path engine. Pure core first, then the file-writing
|
|
2
|
-
// commands against temp repos, then `run` against a real local HTTP server.
|
|
3
|
-
import { test } from 'node:test'
|
|
4
|
-
import assert from 'node:assert/strict'
|
|
5
|
-
import fs from 'node:fs'
|
|
6
|
-
import os from 'node:os'
|
|
7
|
-
import path from 'node:path'
|
|
8
|
-
import http from 'node:http'
|
|
9
|
-
|
|
10
|
-
import {
|
|
11
|
-
resultsEnvelope, openResults, inferHealthPath, USAGE, classifyWorkflow, specTitles, targetProblem, engineIdentity,
|
|
12
|
-
bootApp, stopApp, waitFor,
|
|
13
|
-
validateContract, validateProfile, selectPathsForProfile, computeVerdict, stampTarget,
|
|
14
|
-
journeyCoversId, reconcileJourneys, scanJourneyTags, judgeHttp, judgeCli, playwrightFileResults,
|
|
15
|
-
parseStart, renderView, renderWorkflow, ciProfiles, initPlan, applyPlan, checkDrift,
|
|
16
|
-
migrateLegacy, audit, runPaths, runCli, placeholders, upsertBlock, pointerBlock, workflowFile,
|
|
17
|
-
STANDARD_PROFILES, ENGINE_VERSION, CONTRACT_FILE, VIEW_FILE, WORKFLOW_FILE, ENGINE_FILE,
|
|
18
|
-
} from './golden-path-core.mjs'
|
|
19
|
-
|
|
20
|
-
function contract(over = {}) {
|
|
21
|
-
return {
|
|
22
|
-
version: 3,
|
|
23
|
-
profiles: STANDARD_PROFILES,
|
|
24
|
-
setup: { boot: 'pnpm start', baseUrl: 'http://127.0.0.1:3000', healthPath: '/api/health', journeys: 'e2e', tenants: ['e2e-synthetic'] },
|
|
25
|
-
ci: { queue: 'mergify', packageManager: 'pnpm' },
|
|
26
|
-
paths: [
|
|
27
|
-
{ id: 'health', title: 'The app is up', surface: 'api', start: 'GET /api/health', observable: '200 ok', expect: { status: 200 }, mutates: false, profiles: ['pr-local', 'queue-local', 'prod-preview'] },
|
|
28
|
-
{ id: 'sign-in', title: 'A user signs in and sees the dashboard', surface: 'browser', journey: 'sign-in', mutates: false, profiles: ['pr-local', 'queue-local', 'prod-preview'] },
|
|
29
|
-
{ id: 'checkout', title: 'A user pays for an order', surface: 'browser', journey: 'checkout', mutates: true, tenant: 'e2e-synthetic', cleanup: 'tenant reset after each run', profiles: ['pr-local', 'dev-active', 'prod-preview'] },
|
|
30
|
-
],
|
|
31
|
-
...over,
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function documentation(over = {}) {
|
|
36
|
-
return {
|
|
37
|
-
reference: 'GP-IDENTITY-LOGIN',
|
|
38
|
-
title: { en: 'Sign in', zh: '登录' },
|
|
39
|
-
group: { en: 'Identity', zh: '身份', order: 10 },
|
|
40
|
-
precondition: { en: 'A returning user is signed out', zh: '已有用户尚未登录' },
|
|
41
|
-
summary: {
|
|
42
|
-
route: { en: 'Sign in → Dashboard', zh: '登录 → 仪表盘' },
|
|
43
|
-
outcome: { en: 'The user sees their dashboard', zh: '用户看到自己的仪表盘' },
|
|
44
|
-
},
|
|
45
|
-
steps: [
|
|
46
|
-
{ action: { en: 'Open sign in', zh: '打开登录页' }, expected: { en: 'The credential form is visible', zh: '凭据表单可见' } },
|
|
47
|
-
{ action: { en: 'Submit valid credentials', zh: '提交有效凭据' }, expected: { en: 'The dashboard opens for this user', zh: '打开该用户的仪表盘' } },
|
|
48
|
-
],
|
|
49
|
-
...over,
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function documentedSection(md, id) {
|
|
54
|
-
const anchor = `<a id="golden-path-${id}"></a>`
|
|
55
|
-
assert.ok(md.includes(anchor), `missing stable anchor for ${id}`)
|
|
56
|
-
return md.slice(md.indexOf(anchor) + anchor.length).split(/\n<a id=|\n## /)[0]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function tmpRepo(files = {}) {
|
|
60
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'golden-path-test-'))
|
|
61
|
-
fs.mkdirSync(path.join(dir, '.git'))
|
|
62
|
-
for (const [f, text] of Object.entries(files)) {
|
|
63
|
-
fs.mkdirSync(path.dirname(path.join(dir, f)), { recursive: true })
|
|
64
|
-
fs.writeFileSync(path.join(dir, f), text)
|
|
65
|
-
}
|
|
66
|
-
return dir
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const SPECS = {
|
|
70
|
-
'e2e/sign-in.spec.ts': '// @golden-path\n// @covers sign-in\n',
|
|
71
|
-
'e2e/checkout.spec.ts': '// @golden-path\n// @covers checkout\n',
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function quiet() {
|
|
75
|
-
const out = []
|
|
76
|
-
return { out, log: (s) => out.push(s), err: (s) => out.push(s) }
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ---- validation ----
|
|
80
|
-
|
|
81
|
-
test('a complete version 3 contract validates', () => {
|
|
82
|
-
const v = validateContract(contract())
|
|
83
|
-
assert.deepEqual(v.errors, [])
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
test('an empty contract is invalid, never a vacuous pass', () => {
|
|
87
|
-
assert.equal(validateContract(contract({ paths: [] })).ok, false)
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
test('version 3 requires a short title on every path', () => {
|
|
91
|
-
const c = contract()
|
|
92
|
-
delete c.paths[0].title
|
|
93
|
-
c.paths[1].title = 'x'.repeat(81)
|
|
94
|
-
const errors = validateContract(c).errors.join('\n')
|
|
95
|
-
assert.match(errors, /health.*needs a "title"/)
|
|
96
|
-
assert.match(errors, /sign-in.*title is 81 chars/)
|
|
97
|
-
})
|
|
98
|
-
|
|
99
|
-
test('a path that writes data needs a registered tenant and a cleanup', () => {
|
|
100
|
-
const c = contract()
|
|
101
|
-
delete c.paths[2].cleanup
|
|
102
|
-
c.paths[2].tenant = 'e2e-other'
|
|
103
|
-
const errors = validateContract(c).errors.join('\n')
|
|
104
|
-
assert.match(errors, /checkout.*needs "cleanup"/)
|
|
105
|
-
assert.match(errors, /tenant "e2e-other" is not listed in setup.tenants/)
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
test('TODO placeholders fail validation wherever they are', () => {
|
|
109
|
-
const c = contract()
|
|
110
|
-
c.setup.boot = 'TODO: the command'
|
|
111
|
-
c.paths[0].title = 'TODO: one line'
|
|
112
|
-
assert.deepEqual(placeholders(c), ['setup.boot', 'paths[0].title'])
|
|
113
|
-
assert.match(validateContract(c).errors.join('\n'), /setup.boot: still a TODO placeholder/)
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
test('an observable over 300 characters is an error in version 3 and a warning before', () => {
|
|
117
|
-
const long = contract()
|
|
118
|
-
long.paths[0].observable = 'y'.repeat(301)
|
|
119
|
-
assert.match(validateContract(long).errors.join('\n'), /observable is 301 chars/)
|
|
120
|
-
const legacy = { ...long, version: undefined }
|
|
121
|
-
delete legacy.version
|
|
122
|
-
assert.match(validateContract(legacy).warnings.join('\n'), /observable is 301 chars/)
|
|
123
|
-
})
|
|
124
|
-
|
|
125
|
-
test('a legacy manifest (no version) still validates, with a migrate warning', () => {
|
|
126
|
-
const legacy = { profiles: STANDARD_PROFILES, setup: { boot: 'x', env: ['DATABASE_URL'] }, paths: [{ id: 'health', surface: 'api', start: 'GET /h', observable: '200', profiles: ['pr-local'] }] }
|
|
127
|
-
const v = validateContract(legacy)
|
|
128
|
-
assert.equal(v.ok, true)
|
|
129
|
-
assert.match(v.warnings.join('\n'), /audit --write/)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
test('version 3 rejects the legacy env name list', () => {
|
|
133
|
-
const c = contract()
|
|
134
|
-
c.setup.env = ['DATABASE_URL']
|
|
135
|
-
assert.match(validateContract(c).errors.join('\n'), /setup.env: in version 3/)
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
test('a path must declare at least one known profile', () => {
|
|
139
|
-
const c = contract()
|
|
140
|
-
c.paths[0].profiles = []
|
|
141
|
-
c.paths[1].profiles = ['nightly']
|
|
142
|
-
const errors = validateContract(c).errors.join('\n')
|
|
143
|
-
assert.match(errors, /health.*non-empty "profiles"/)
|
|
144
|
-
assert.match(errors, /profile "nightly" is not declared/)
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
test('reserved profile names stay bound to their gate', () => {
|
|
148
|
-
const errors = validateProfile('prod-preview', { gate: 'trunk-integration', environment: 'dev', endpoint: 'active', mutationPolicy: 'synthetic-tenant' })
|
|
149
|
-
assert.match(errors.join('\n'), /reserved name/)
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
test('a production profile must be read-only', () => {
|
|
153
|
-
const errors = validateProfile('prod-x', { gate: 'post-promotion', environment: 'prod', endpoint: 'active', mutationPolicy: 'isolated-writes' })
|
|
154
|
-
assert.match(errors.join('\n'), /prod profile must be read-only/)
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
test('a gate must run in its own environment and endpoint', () => {
|
|
158
|
-
const errors = validateProfile('pr-x', { gate: 'pull-request', environment: 'prod', endpoint: 'active', mutationPolicy: 'read-only' })
|
|
159
|
-
assert.match(errors.join('\n'), /runs in environment "local"/)
|
|
160
|
-
assert.match(errors.join('\n'), /targets the "local" endpoint/)
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
// ---- selection and verdict ----
|
|
164
|
-
|
|
165
|
-
test('read-only profiles exclude every path that writes, tenant or not', () => {
|
|
166
|
-
const { run, excluded } = selectPathsForProfile(contract(), 'prod-preview')
|
|
167
|
-
assert.deepEqual(run.map((p) => p.id), ['health', 'sign-in'])
|
|
168
|
-
assert.match(excluded.find((x) => x.id === 'checkout').reason, /read-only/)
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
test('a synthetic-tenant profile runs a writing path only with a tenant', () => {
|
|
172
|
-
const c = contract()
|
|
173
|
-
assert.deepEqual(selectPathsForProfile(c, 'dev-active').run.map((p) => p.id), ['checkout'])
|
|
174
|
-
delete c.paths[2].tenant
|
|
175
|
-
assert.deepEqual(selectPathsForProfile(c, 'dev-active').run, [])
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
test('an unknown profile is a hard stop', () => {
|
|
179
|
-
assert.throws(() => selectPathsForProfile(contract(), 'nightly'), /unknown profile "nightly"/)
|
|
180
|
-
})
|
|
181
|
-
|
|
182
|
-
test('verdict: all pass is ALIVE, a fail is DEAD', () => {
|
|
183
|
-
const sel = [{ id: 'a', surface: 'api' }, { id: 'b', surface: 'api' }]
|
|
184
|
-
assert.equal(computeVerdict(sel, [{ id: 'a', status: 'pass' }, { id: 'b', status: 'pass' }]).verdict, 'ALIVE')
|
|
185
|
-
const dead = computeVerdict(sel, [{ id: 'a', status: 'pass' }, { id: 'b', status: 'fail', note: 'HTTP 500' }])
|
|
186
|
-
assert.equal(dead.verdict, 'DEAD')
|
|
187
|
-
assert.deepEqual(dead.broken.map((b) => b.id), ['b'])
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
test('verdict: a missing or cannot-run path is CANNOT-RUN, not DEAD', () => {
|
|
191
|
-
const sel = [{ id: 'a', surface: 'api' }, { id: 'b', surface: 'api' }]
|
|
192
|
-
const v = computeVerdict(sel, [{ id: 'a', status: 'pass' }, { id: 'b', status: 'cannot-run', note: 'no secret' }])
|
|
193
|
-
assert.equal(v.verdict, 'CANNOT-RUN')
|
|
194
|
-
assert.equal(v.ok, false)
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
test('verdict: a failed path is DEAD even when another could not run', () => {
|
|
198
|
-
const sel = [{ id: 'a', surface: 'api' }, { id: 'b', surface: 'api' }]
|
|
199
|
-
const v = computeVerdict(sel, [{ id: 'a', status: 'fail', note: 'HTTP 500' }])
|
|
200
|
-
assert.equal(v.verdict, 'DEAD')
|
|
201
|
-
assert.deepEqual(v.broken.map((b) => [b.id, b.status]), [['a', 'fail'], ['b', 'missing']])
|
|
202
|
-
})
|
|
203
|
-
|
|
204
|
-
test('verdict: nothing selected is EMPTY, never ALIVE', () => {
|
|
205
|
-
const v = computeVerdict([], [])
|
|
206
|
-
assert.equal(v.verdict, 'EMPTY')
|
|
207
|
-
assert.equal(v.ok, false)
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
test('the verdict is stamped with target, engine, and time', () => {
|
|
211
|
-
const v = stampTarget({ verdict: 'ALIVE' }, 'https://x', { profile: 'prod-preview', generatedAt: 'forged' })
|
|
212
|
-
assert.equal(v.target, 'https://x')
|
|
213
|
-
assert.equal(v.engine, ENGINE_VERSION)
|
|
214
|
-
assert.notEqual(v.generatedAt, 'forged')
|
|
215
|
-
})
|
|
216
|
-
|
|
217
|
-
// ---- journeys ----
|
|
218
|
-
|
|
219
|
-
test('journey ids accept the new tag, the legacy tag, and a bare id', () => {
|
|
220
|
-
assert.equal(journeyCoversId('@golden-path sign-in'), 'sign-in')
|
|
221
|
-
assert.equal(journeyCoversId('@smoke smoke/sign-in'), 'smoke/sign-in')
|
|
222
|
-
assert.equal(journeyCoversId('checkout'), 'checkout')
|
|
223
|
-
})
|
|
224
|
-
|
|
225
|
-
test('reconcile fails both ways: an untagged journey and an orphan tag', () => {
|
|
226
|
-
const r = reconcileJourneys(contract(), ['sign-in', 'refund'])
|
|
227
|
-
const errors = r.errors.join('\n')
|
|
228
|
-
assert.match(errors, /checkout.*no spec is tagged/)
|
|
229
|
-
assert.match(errors, /@covers refund is not in the contract/)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('scanJourneyTags reads @golden-path and @smoke specs, skipping untagged ones', () => {
|
|
233
|
-
const dir = tmpRepo({
|
|
234
|
-
'e2e/a.spec.ts': '// @golden-path\n// @covers sign-in\n',
|
|
235
|
-
'e2e/b.spec.ts': '// @smoke\n// @covers checkout\n',
|
|
236
|
-
'e2e/c.spec.ts': '// @covers not-tagged\n',
|
|
237
|
-
'e2e/d.spec.ts': '// @golden-path\n',
|
|
238
|
-
})
|
|
239
|
-
const { byId, noCovers } = scanJourneyTags(path.join(dir, 'e2e'))
|
|
240
|
-
assert.deepEqual([...byId.keys()].sort(), ['checkout', 'sign-in'])
|
|
241
|
-
assert.equal(noCovers.length, 1)
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
// ---- judging observations ----
|
|
245
|
-
|
|
246
|
-
test('parseStart reads one request and refuses prose', () => {
|
|
247
|
-
assert.deepEqual(parseStart('GET /api/health'), { method: 'GET', path: '/api/health' })
|
|
248
|
-
assert.equal(parseStart('GET /a, GET /b'), null)
|
|
249
|
-
})
|
|
250
|
-
|
|
251
|
-
test('judgeHttp checks status, body text, and a JSON subset', () => {
|
|
252
|
-
const p = { expect: { status: [200, 204], bodyIncludes: 'ok', json: { status: 'ok', db: { up: true } } } }
|
|
253
|
-
assert.equal(judgeHttp(p, 200, '{"status":"ok","db":{"up":true,"ms":3}}').status, 'pass')
|
|
254
|
-
assert.match(judgeHttp(p, 500, '').note, /HTTP 500, expected 200 or 204/)
|
|
255
|
-
assert.match(judgeHttp(p, 200, '{"status":"ok","db":{"up":false}}').note, /does not contain/)
|
|
256
|
-
assert.equal(judgeHttp({}, 201, '').status, 'pass')
|
|
257
|
-
assert.equal(judgeHttp({}, 302, '').status, 'fail')
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
test('judgeCli checks the exit code and the output', () => {
|
|
261
|
-
assert.equal(judgeCli({}, 0, '').status, 'pass')
|
|
262
|
-
assert.match(judgeCli({ expect: { exitCode: 0, stdoutIncludes: ['migrated'] } }, 0, 'nothing').note, /does not include/)
|
|
263
|
-
assert.match(judgeCli({}, 3, '').note, /exit 3/)
|
|
264
|
-
})
|
|
265
|
-
|
|
266
|
-
const pass = { status: 'expected', results: [{ status: 'passed' }] }
|
|
267
|
-
const failed = { status: 'unexpected', results: [{ status: 'failed' }] }
|
|
268
|
-
|
|
269
|
-
test('playwrightFileResults resolves files against rootDir and folds every test', () => {
|
|
270
|
-
const report = { config: { rootDir: '/app/e2e' }, suites: [
|
|
271
|
-
{ file: 'a.spec.ts', specs: [{ tests: [pass] }], suites: [{ specs: [{ tests: [{ status: 'flaky', results: [{ status: 'failed' }, { status: 'passed' }] }] }] }] },
|
|
272
|
-
{ file: 'admin/b.spec.ts', specs: [{ tests: [pass] }, { tests: [failed] }] },
|
|
273
|
-
{ file: '../c.spec.ts', specs: [{ tests: [pass] }] },
|
|
274
|
-
] }
|
|
275
|
-
const r = playwrightFileResults(report, { appDir: '/app' })
|
|
276
|
-
assert.equal(r.get('/app/e2e/a.spec.ts'), 'pass')
|
|
277
|
-
assert.equal(r.get('/app/e2e/admin/b.spec.ts'), 'fail')
|
|
278
|
-
assert.equal(r.get('/app/c.spec.ts'), 'pass')
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
test('a skipped test or an expected failure is never a pass', () => {
|
|
282
|
-
const skipped = { config: { rootDir: '/a' }, suites: [{ file: 's.spec.ts', specs: [{ ok: true, tests: [{ status: 'skipped', results: [{ status: 'skipped' }] }] }] }] }
|
|
283
|
-
assert.equal(playwrightFileResults(skipped, { appDir: '/a' }).get('/a/s.spec.ts'), 'skipped')
|
|
284
|
-
const expectedFailure = { config: { rootDir: '/a' }, suites: [{ file: 'f.spec.ts', specs: [{ ok: true, tests: [{ status: 'expected', results: [{ status: 'failed' }] }] }] }] }
|
|
285
|
-
assert.equal(playwrightFileResults(expectedFailure, { appDir: '/a' }).get('/a/f.spec.ts'), 'fail')
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
// ---- generated files ----
|
|
289
|
-
|
|
290
|
-
test('the view lists every path and every profile, and says it is generated', () => {
|
|
291
|
-
const md = renderView(contract())
|
|
292
|
-
assert.match(md, /GENERATED by the golden-path skill/)
|
|
293
|
-
assert.match(md, /\| `checkout` \| A user pays for an order \| Browser journey `checkout` \|.*\| yes \(tenant `e2e-synthetic`\) \|/)
|
|
294
|
-
assert.match(md, /\| `prod-preview` \| Production candidate at 0% traffic.* \| never \| 2 \| the deploy or release pipeline \|/)
|
|
295
|
-
assert.match(md, /\| `pr-local` .*`\.github\/workflows\/golden-paths\.yml` \|/)
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
test('the view escapes pipes so a title cannot break the table', () => {
|
|
299
|
-
const c = contract()
|
|
300
|
-
c.paths[0].title = 'up | down'
|
|
301
|
-
assert.match(renderView(c), /up \\\| down/)
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
// ---- optional human documentation ----
|
|
305
|
-
|
|
306
|
-
test('documentation references and id anchors survive insertion, reorder, regrouping, and title changes', () => {
|
|
307
|
-
const original = contract()
|
|
308
|
-
original.paths[1].documentation = documentation()
|
|
309
|
-
original.paths[2].documentation = documentation({ reference: 'GP-COMMERCE-PAY', title: { en: 'Pay', zh: '支付' } })
|
|
310
|
-
const changes = {
|
|
311
|
-
unchanged: () => {},
|
|
312
|
-
insertion: (c) => c.paths.unshift({ ...structuredClone(c.paths[1]), id: 'new-user', journey: 'new-user', documentation: documentation({ reference: 'GP-IDENTITY-REGISTER', title: { en: 'Register', zh: '注册' } }) }),
|
|
313
|
-
reorder: (c) => c.paths.reverse(),
|
|
314
|
-
regrouping: (c) => { c.paths[1].documentation.group = { en: 'Account', zh: '账户', order: 1 } },
|
|
315
|
-
renaming: (c) => { c.paths[1].title = 'Returning user login'; c.paths[1].documentation.title = { en: 'Log in', zh: '用户登录' } },
|
|
316
|
-
}
|
|
317
|
-
for (const [change, mutate] of Object.entries(changes)) {
|
|
318
|
-
const c = structuredClone(original)
|
|
319
|
-
mutate(c)
|
|
320
|
-
assert.deepEqual(validateContract(c).errors, [], change)
|
|
321
|
-
const md = renderView(c)
|
|
322
|
-
for (const id of ['sign-in', 'checkout']) {
|
|
323
|
-
const p = c.paths.find((p) => p.id === id)
|
|
324
|
-
const { reference, title } = p.documentation
|
|
325
|
-
assert.ok(documentedSection(md, id).includes(`### ${reference}: ${title.en}\n`), change)
|
|
326
|
-
assert.ok(md.includes(`- [${reference}: ${title.en} / ${title.zh}](#golden-path-${id})`), change)
|
|
327
|
-
assert.equal((md.match(new RegExp(`<a id="golden-path-${id}"></a>`, 'g')) || []).length, 1, change)
|
|
328
|
-
}
|
|
329
|
-
const selected = selectPathsForProfile(c, 'prod-preview').run.filter((p) => p.id !== 'new-user')
|
|
330
|
-
assert.deepEqual(selected.map((p) => p.id).sort(), ['health', 'sign-in'], change)
|
|
331
|
-
const verdict = computeVerdict(selected, [{ id: 'health', status: 'pass' }, { id: 'sign-in', status: 'pass' }])
|
|
332
|
-
assert.equal(verdict.verdict, 'ALIVE', change)
|
|
333
|
-
assert.deepEqual(verdict.rows.map((p) => p.id).sort(), ['health', 'sign-in'], change)
|
|
334
|
-
}
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
test('missing references use machine ids for documented and mixed legacy paths, never positional labels', () => {
|
|
338
|
-
const c = contract()
|
|
339
|
-
c.paths[1].documentation = documentation()
|
|
340
|
-
delete c.paths[1].documentation.reference
|
|
341
|
-
for (const paths of [c.paths, [...c.paths].reverse()]) {
|
|
342
|
-
const md = renderView({ ...c, paths })
|
|
343
|
-
assert.ok(md.includes('- [sign-in: Sign in / 登录](#golden-path-sign-in)'))
|
|
344
|
-
assert.match(documentedSection(md, 'sign-in'), /### sign-in: Sign in\n/)
|
|
345
|
-
assert.ok(md.includes(`- [health: ${c.paths[0].title}](#golden-path-health)`))
|
|
346
|
-
assert.ok(documentedSection(md, 'health').includes(`### health: ${c.paths[0].title}\n`))
|
|
347
|
-
assert.doesNotMatch(md, /GP-0?[123]:/)
|
|
348
|
-
}
|
|
349
|
-
})
|
|
350
|
-
|
|
351
|
-
test('entirely undocumented contracts retain the legacy table, including versionless manifests', () => {
|
|
352
|
-
for (const version of [3, undefined]) {
|
|
353
|
-
const c = contract({ version })
|
|
354
|
-
if (version === undefined) {
|
|
355
|
-
for (const p of c.paths) delete p.title
|
|
356
|
-
c.paths[1].intent = 'A returning user signs in. Further context.'
|
|
357
|
-
}
|
|
358
|
-
const md = renderView(c)
|
|
359
|
-
assert.match(md, /\| Path \| What it proves \| How it is checked \| Runs in \| Writes data \|/)
|
|
360
|
-
assert.ok(md.includes(`| \`sign-in\` | ${version === 3 ? c.paths[1].title : 'A returning user signs in.'} | Browser journey \`sign-in\` |`))
|
|
361
|
-
assert.doesNotMatch(md, /<details>|golden-path-sign-in|## Contents|### /)
|
|
362
|
-
}
|
|
363
|
-
})
|
|
364
|
-
|
|
365
|
-
test('browser groups follow group order and services remain a separate appendix', () => {
|
|
366
|
-
const c = contract()
|
|
367
|
-
c.paths[1].documentation = documentation()
|
|
368
|
-
c.paths[2].documentation = documentation({ reference: 'GP-COMMERCE-PAY', group: { en: 'Commerce', zh: '交易', order: 2 }, title: { en: 'Pay', zh: '支付' } })
|
|
369
|
-
c.paths[0].documentation = documentation({ reference: 'GP-SERVICE-HEALTH', group: { en: 'Commerce', zh: '交易', order: 0 }, title: { en: 'Health', zh: '健康检查' } })
|
|
370
|
-
const md = renderView(c)
|
|
371
|
-
const commerce = md.indexOf('\n## Commerce / 交易\n')
|
|
372
|
-
const identity = md.indexOf('\n## Identity / 身份\n')
|
|
373
|
-
const services = md.indexOf('\n## Service checks / 服务检查\n')
|
|
374
|
-
assert.ok(commerce >= 0 && identity > commerce && services > identity)
|
|
375
|
-
assert.ok(md.indexOf('<a id="golden-path-checkout">') > commerce && md.indexOf('<a id="golden-path-checkout">') < identity)
|
|
376
|
-
assert.ok(md.indexOf('<a id="golden-path-sign-in">') > identity && md.indexOf('<a id="golden-path-sign-in">') < services)
|
|
377
|
-
assert.ok(md.indexOf('<a id="golden-path-health">') > services)
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
test('bilingual summaries stay visible while original steps and verification are in closed details', () => {
|
|
381
|
-
const c = contract()
|
|
382
|
-
const doc = documentation()
|
|
383
|
-
c.paths[1].documentation = doc
|
|
384
|
-
const section = documentedSection(renderView(c), 'sign-in')
|
|
385
|
-
const details = section.match(/<details>\s*<summary>Full steps \/ 完整步骤<\/summary>([\s\S]*?)<\/details>/)
|
|
386
|
-
assert.ok(details, 'full steps must be in a closed disclosure')
|
|
387
|
-
const visible = section.slice(0, details.index) + section.slice(details.index + details[0].length)
|
|
388
|
-
for (const text of [doc.summary.route.en, doc.summary.route.zh, doc.summary.outcome.en, doc.summary.outcome.zh]) {
|
|
389
|
-
assert.ok(visible.includes(text), `summary must remain visible: ${text}`)
|
|
390
|
-
}
|
|
391
|
-
for (const text of [doc.precondition.en, doc.precondition.zh, ...doc.steps.flatMap((s) => [s.action.en, s.action.zh, s.expected.en, s.expected.zh])]) {
|
|
392
|
-
assert.ok(details[1].includes(text), `full original copy must be retained: ${text}`)
|
|
393
|
-
assert.ok(!visible.includes(text), `full copy must be collapsed: ${text}`)
|
|
394
|
-
}
|
|
395
|
-
assert.match(details[1], /Browser journey `sign-in`/)
|
|
396
|
-
assert.match(details[1], /pr-local, queue-local, prod-preview/)
|
|
397
|
-
assert.match(details[1], /Writes to shared data \/ 写入共享数据: no \/ 否/)
|
|
398
|
-
assert.doesNotMatch(section, /<details\s+[^>]*open/i)
|
|
399
|
-
})
|
|
400
|
-
|
|
401
|
-
test('paths without summaries stay expanded and English-only summaries need no Chinese', () => {
|
|
402
|
-
const c = contract()
|
|
403
|
-
const doc = documentation()
|
|
404
|
-
delete doc.summary
|
|
405
|
-
c.paths[1].documentation = doc
|
|
406
|
-
let section = documentedSection(renderView(c), 'sign-in')
|
|
407
|
-
assert.doesNotMatch(section, /<details>|<summary>/)
|
|
408
|
-
assert.ok(section.includes(doc.steps[1].expected.en))
|
|
409
|
-
assert.match(section, /Browser journey `sign-in`/)
|
|
410
|
-
c.paths[1].documentation = {
|
|
411
|
-
summary: { route: { en: 'Login → Home' }, outcome: { en: 'Home opens' } },
|
|
412
|
-
steps: [{ action: { en: 'Log in' }, expected: { en: 'Home is visible' } }],
|
|
413
|
-
}
|
|
414
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
415
|
-
section = documentedSection(renderView(c), 'sign-in')
|
|
416
|
-
assert.match(section, /<details>\s*<summary>Full steps<\/summary>/)
|
|
417
|
-
const route = section.indexOf('Login → Home')
|
|
418
|
-
assert.ok(route >= 0 && route < section.indexOf('<details>'))
|
|
419
|
-
})
|
|
420
|
-
|
|
421
|
-
test('documentation accepts stable uppercase references and rejects malformed references', () => {
|
|
422
|
-
for (const reference of ['GP-A', 'GP-9', 'GP-IDENTITY-LOGIN', 'GP-V2-A1']) {
|
|
423
|
-
const c = contract()
|
|
424
|
-
c.paths[1].documentation = documentation({ reference })
|
|
425
|
-
assert.deepEqual(validateContract(c).errors, [], reference)
|
|
426
|
-
}
|
|
427
|
-
for (const reference of ['', 'GP-', 'gp-IDENTITY-LOGIN', 'GP-Identity', 'GP-A--B', 'GP-A-', 'GP_A', 'GP-A B', ' GP-A', 'GP-A\n', 1, null, ['GP-A']]) {
|
|
428
|
-
const c = contract()
|
|
429
|
-
c.paths[1].documentation = documentation({ reference })
|
|
430
|
-
const result = validateContract(c)
|
|
431
|
-
assert.equal(result.ok, false, JSON.stringify(reference))
|
|
432
|
-
assert.match(result.errors.join('\n'), /documentation\.reference/, JSON.stringify(reference))
|
|
433
|
-
}
|
|
434
|
-
})
|
|
435
|
-
|
|
436
|
-
test('duplicate documentation references are invalid across browser and service paths', () => {
|
|
437
|
-
const c = contract()
|
|
438
|
-
c.paths[0].documentation = documentation()
|
|
439
|
-
c.paths[1].documentation = documentation()
|
|
440
|
-
const result = validateContract(c)
|
|
441
|
-
assert.equal(result.ok, false)
|
|
442
|
-
assert.match(result.errors.join('\n'), /duplicate documentation\.reference.*GP-IDENTITY-LOGIN/)
|
|
443
|
-
c.paths[0].documentation.reference = 'GP-SERVICE-HEALTH'
|
|
444
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
test('documentation validation rejects malformed localized fields, unpaired copy, and incomplete steps', () => {
|
|
448
|
-
const cases = [
|
|
449
|
-
['documentation', () => null],
|
|
450
|
-
['documentation', () => []],
|
|
451
|
-
['documentation', () => 'notes'],
|
|
452
|
-
['documentation.title', (d) => ({ ...d, title: { zh: '登录' } })],
|
|
453
|
-
['documentation.title', (d) => ({ ...d, title: { en: ' ' } })],
|
|
454
|
-
['documentation.title', (d) => ({ ...d, title: { en: 'Login', zh: '' } })],
|
|
455
|
-
['documentation.group', (d) => ({ ...d, group: 'Identity' })],
|
|
456
|
-
['documentation.group.order', (d) => ({ ...d, group: { en: 'Identity', order: -1 } })],
|
|
457
|
-
['documentation.group.order', (d) => ({ ...d, group: { en: 'Identity', order: 1.5 } })],
|
|
458
|
-
['documentation.group.order', (d) => ({ ...d, group: { en: 'Identity', order: '1' } })],
|
|
459
|
-
['documentation.precondition', (d) => ({ ...d, precondition: { en: 42 } })],
|
|
460
|
-
['documentation.summary', (d) => ({ ...d, summary: [] })],
|
|
461
|
-
['documentation.summary.route', (d) => ({ ...d, summary: { outcome: { en: 'Home opens' } } })],
|
|
462
|
-
['documentation.summary.outcome', (d) => ({ ...d, summary: { route: { en: 'Login' } } })],
|
|
463
|
-
['documentation.summary.route', (d) => ({ ...d, summary: { ...d.summary, route: { en: '' } } })],
|
|
464
|
-
['documentation.summary', (d) => ({ ...d, summary: { ...d.summary, outcome: { en: 'Home opens' } } })],
|
|
465
|
-
['documentation.summary', (d) => ({ ...d, summary: { ...d.summary, route: { en: 'Login' } } })],
|
|
466
|
-
['documentation.steps', (d) => ({ ...d, steps: undefined })],
|
|
467
|
-
['documentation.steps', (d) => ({ ...d, steps: [] })],
|
|
468
|
-
['documentation.steps', (d) => ({ ...d, steps: {} })],
|
|
469
|
-
['documentation.steps[0].action', (d) => ({ ...d, steps: [null] })],
|
|
470
|
-
['documentation.steps[0].action', (d) => ({ ...d, steps: [{ action: { en: ' ' }, expected: { en: 'Home' } }] })],
|
|
471
|
-
['documentation.steps[0].expected', (d) => ({ ...d, steps: [{ action: { en: 'Login' } }] })],
|
|
472
|
-
['documentation.steps[0]', (d) => ({ ...d, steps: [{ action: { en: 'Login', zh: '登录' }, expected: { en: 'Home' } }] })],
|
|
473
|
-
['documentation.steps[1]', (d) => ({ ...d, steps: [d.steps[0], { action: { en: 'Login' }, expected: { en: 'Home' } }] })],
|
|
474
|
-
]
|
|
475
|
-
for (const [field, invalid] of cases) {
|
|
476
|
-
const c = contract()
|
|
477
|
-
c.paths[1].documentation = invalid(documentation())
|
|
478
|
-
const result = validateContract(c)
|
|
479
|
-
assert.equal(result.ok, false, field)
|
|
480
|
-
assert.ok(result.errors.some((error) => error.includes(field)), `${field}: ${result.errors.join('\n')}`)
|
|
481
|
-
}
|
|
482
|
-
const c = contract()
|
|
483
|
-
c.paths[1].documentation = { group: { en: 'Identity', order: 0 }, steps: [{ action: { en: 'Login' }, expected: { en: 'Home' } }] }
|
|
484
|
-
assert.deepEqual(validateContract(c).errors, [], 'localized presentation fields and reference are optional')
|
|
485
|
-
delete c.paths[1].journey
|
|
486
|
-
assert.match(validateContract(c).errors.join('\n'), /browser path needs "journey"/, 'documentation cannot replace the executable journey')
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
test('documentation HTML is escaped in headings, Contents, summaries, and collapsed steps', () => {
|
|
490
|
-
const c = contract()
|
|
491
|
-
const unsafe = '<script>alert("x")</script> & <details open> | literal'
|
|
492
|
-
const safe = '<script>alert("x")</script> & <details open> \\| literal'
|
|
493
|
-
const copy = (name) => ({ en: `${name}: ${unsafe}`, zh: `${name}中文: ${unsafe}` })
|
|
494
|
-
c.paths[1].documentation = documentation({
|
|
495
|
-
title: copy('Title'), group: copy('Group'), precondition: copy('Precondition'),
|
|
496
|
-
summary: { route: copy('Route'), outcome: copy('Outcome') },
|
|
497
|
-
steps: [{ action: copy('Action'), expected: copy('Expected result') }],
|
|
498
|
-
})
|
|
499
|
-
const md = renderView(c)
|
|
500
|
-
assert.ok(md.includes(`### GP-IDENTITY-LOGIN: Title: ${safe}\n`))
|
|
501
|
-
assert.ok(md.includes(`- [GP-IDENTITY-LOGIN: Title: ${safe} / Title中文: ${safe}](#golden-path-sign-in)`))
|
|
502
|
-
assert.ok(md.includes(`## Group: ${safe} / Group中文: ${safe}\n`))
|
|
503
|
-
const section = documentedSection(md, 'sign-in')
|
|
504
|
-
const details = section.match(/<details>\s*<summary>Full steps \/ 完整步骤<\/summary>([\s\S]*?)<\/details>/)
|
|
505
|
-
assert.ok(details)
|
|
506
|
-
const visible = section.slice(0, details.index)
|
|
507
|
-
for (const name of ['Route', 'Outcome']) {
|
|
508
|
-
assert.ok(visible.includes(`${name}: ${safe}`), name)
|
|
509
|
-
assert.ok(visible.includes(`${name}中文: ${safe}`), name)
|
|
510
|
-
}
|
|
511
|
-
for (const name of ['Precondition', 'Action', 'Expected result']) {
|
|
512
|
-
assert.ok(details[1].includes(`${name}: ${safe}`), name)
|
|
513
|
-
assert.ok(details[1].includes(`${name}中文: ${safe}`), name)
|
|
514
|
-
}
|
|
515
|
-
assert.doesNotMatch(md, /<script>|<details open>/)
|
|
516
|
-
})
|
|
517
|
-
|
|
518
|
-
test('literal Markdown cannot redirect Contents links or format journey copy', () => {
|
|
519
|
-
const c = contract()
|
|
520
|
-
const text = 'Review ](https://example.com) [case] **draft** `tag` _notes_ ~~old~~ \\ ready'
|
|
521
|
-
const escaped = 'Review \\](https://example.com) \\[case\\] \\*\\*draft\\*\\* \\`tag\\` \\_notes\\_ \\~\\~old\\~\\~ \\\\ ready'
|
|
522
|
-
c.paths[1].documentation = documentation({
|
|
523
|
-
title: { en: text }, group: { en: text }, precondition: { en: '# Literal heading' },
|
|
524
|
-
summary: { route: { en: text }, outcome: { en: text } },
|
|
525
|
-
steps: [{ action: { en: text }, expected: { en: '1. Literal result' } }],
|
|
526
|
-
})
|
|
527
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
528
|
-
const md = renderView(c)
|
|
529
|
-
assert.ok(md.includes(`- [GP-IDENTITY-LOGIN: ${escaped}](#golden-path-sign-in)`))
|
|
530
|
-
assert.ok(md.includes(`### GP-IDENTITY-LOGIN: ${escaped}\n`))
|
|
531
|
-
assert.ok(md.includes(`**Route:** ${escaped}\n`))
|
|
532
|
-
assert.ok(md.includes(`1. **${escaped}**`))
|
|
533
|
-
assert.ok(md.includes('\\# Literal heading'))
|
|
534
|
-
assert.ok(md.includes('Expected: 1\\. Literal result'))
|
|
535
|
-
assert.doesNotMatch(md, /(?<!\\)\]\(https:\/\/example\.com\)/)
|
|
536
|
-
for (const prefix of ['-', '+', '###']) {
|
|
537
|
-
c.paths[1].documentation.precondition.en = `${prefix} Literal text`
|
|
538
|
-
assert.ok(renderView(c).includes(`\\${prefix} Literal text`))
|
|
539
|
-
}
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
test('malformed Chinese values return field errors instead of invoking trim properties', () => {
|
|
543
|
-
for (const value of [{ trim: 42 }, { trim: 'not callable' }, {}, [], null, 42, false]) {
|
|
544
|
-
for (const field of ['route', 'outcome', 'action', 'expected']) {
|
|
545
|
-
const c = contract()
|
|
546
|
-
c.paths[1].documentation = documentation()
|
|
547
|
-
const doc = c.paths[1].documentation
|
|
548
|
-
if (field === 'route' || field === 'outcome') doc.summary[field].zh = value
|
|
549
|
-
else doc.steps[0][field].zh = value
|
|
550
|
-
const result = validateContract(c)
|
|
551
|
-
assert.equal(result.ok, false, `${field}: ${JSON.stringify(value)}`)
|
|
552
|
-
assert.ok(result.errors.some((error) => error.includes(`.${field}`)))
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
test('invalid documentation refuses rendering and init with actionable field diagnostics', () => {
|
|
558
|
-
const dir = tmpRepo({ 'GOLDEN_PATHS.md': 'existing guide\n' })
|
|
559
|
-
try {
|
|
560
|
-
const c = contract()
|
|
561
|
-
c.paths[1].documentation = documentation()
|
|
562
|
-
delete c.paths[1].documentation.summary.outcome
|
|
563
|
-
assert.throws(() => renderView(c), /invalid documentation:[\s\S]*documentation\.summary\.outcome/)
|
|
564
|
-
assert.throws(() => initPlan(path.join(dir, CONTRACT_FILE), { contract: c }), /invalid documentation:[\s\S]*documentation\.summary\.outcome/)
|
|
565
|
-
assert.equal(fs.readFileSync(path.join(dir, VIEW_FILE), 'utf8'), 'existing guide\n')
|
|
566
|
-
assert.equal(fs.existsSync(path.join(dir, ENGINE_FILE)), false)
|
|
567
|
-
c.paths[1].documentation = documentation()
|
|
568
|
-
c.paths[2].documentation = documentation()
|
|
569
|
-
assert.throws(() => renderView(c), /invalid documentation:[\s\S]*duplicate documentation\.reference/)
|
|
570
|
-
} finally { fs.rmSync(dir, { recursive: true, force: true }) }
|
|
571
|
-
})
|
|
572
|
-
|
|
573
|
-
test('workflow triggers follow the declared local profiles and the queue', () => {
|
|
574
|
-
const c = contract()
|
|
575
|
-
assert.deepEqual(ciProfiles(c), ['pr-local', 'queue-local'])
|
|
576
|
-
let wf = renderWorkflow(c)
|
|
577
|
-
assert.match(wf, /^ pull_request:$/m)
|
|
578
|
-
assert.match(wf, /mergify\/merge-queue/)
|
|
579
|
-
c.ci.queue = 'github'
|
|
580
|
-
wf = renderWorkflow(c)
|
|
581
|
-
assert.match(wf, /^ merge_group:$/m)
|
|
582
|
-
c.ci.queue = 'none'
|
|
583
|
-
assert.doesNotMatch(renderWorkflow(c), /merge_group|mergify/)
|
|
584
|
-
assert.equal(renderWorkflow({ ...c, ci: { enabled: false } }), null)
|
|
585
|
-
})
|
|
586
|
-
|
|
587
|
-
test('a postgres 18 database puts its tmpfs where that image keeps PGDATA', () => {
|
|
588
|
-
const c = contract()
|
|
589
|
-
c.setup.database = { engine: 'postgres', version: '18' }
|
|
590
|
-
const wf = renderWorkflow(c)
|
|
591
|
-
assert.match(wf, /--tmpfs \/var\/lib\/postgresql:rw,noexec,nosuid,size=1g/)
|
|
592
|
-
assert.doesNotMatch(wf, /--tmpfs \/var\/lib\/postgresql\/data/)
|
|
593
|
-
})
|
|
594
|
-
|
|
595
|
-
test('the workflow boots, waits, runs, and lets the verdict decide', () => {
|
|
596
|
-
const c = contract()
|
|
597
|
-
c.setup.database = { engine: 'postgres', version: '16' }
|
|
598
|
-
c.setup.secrets = { generate: ['AUTH_SECRET'], fromCi: ['STRIPE_KEY'] }
|
|
599
|
-
c.setup.env = { FEATURE_X: 'on' }
|
|
600
|
-
c.setup.seed = 'pnpm db:seed'
|
|
601
|
-
c.setup.boot = "pnpm start --host '0.0.0.0'"
|
|
602
|
-
const wf = renderWorkflow(c)
|
|
603
|
-
assert.match(wf, /image: postgres:16/)
|
|
604
|
-
// Runners share the host network: the service gets a dynamic host port, never 5432:5432.
|
|
605
|
-
assert.match(wf, /ports: \['5432\/tcp'\]/)
|
|
606
|
-
assert.match(wf, /DATABASE_URL: postgres:\/\/postgres:postgres@127\.0\.0\.1:\$\{\{ job\.services\.postgres\.ports\['5432'\] \}\}\/golden_paths/)
|
|
607
|
-
assert.match(wf, /--tmpfs \/var\/lib\/postgresql\/data:rw,noexec,nosuid,size=1g/)
|
|
608
|
-
assert.match(wf, /for name in AUTH_SECRET; do/)
|
|
609
|
-
assert.match(wf, /STRIPE_KEY: \$\{\{ secrets\.STRIPE_KEY \}\}/)
|
|
610
|
-
assert.match(wf, /FEATURE_X: "on"/)
|
|
611
|
-
assert.match(wf, /nohup sh -c 'pnpm start --host '\\''0\.0\.0\.0'\\''' >/)
|
|
612
|
-
assert.match(wf, /wait --url "http:\/\/127\.0\.0\.1:3000\/api\/health"/)
|
|
613
|
-
assert.match(wf, /playwright install --with-deps chromium/)
|
|
614
|
-
const order = ['check', 'pnpm db:seed', 'Boot the app', ' run --profile', ' verdict --profile'].map((s) => wf.indexOf(s))
|
|
615
|
-
assert.deepEqual([...order].sort((a, b) => a - b), order)
|
|
616
|
-
})
|
|
617
|
-
|
|
618
|
-
test('a monorepo app gets a working directory and prefixed artifact paths', () => {
|
|
619
|
-
const wf = renderWorkflow(contract(), { appRel: 'apps/web' })
|
|
620
|
-
assert.match(wf, /working-directory: "apps\/web"/)
|
|
621
|
-
assert.match(wf, /cache-dependency-path: "apps\/web\/pnpm-lock\.yaml"/)
|
|
622
|
-
assert.match(wf, /apps\/web\/golden-path-results\.json/)
|
|
623
|
-
})
|
|
624
|
-
|
|
625
|
-
test('the AGENTS.md pointer is replaced in place, one block per app', () => {
|
|
626
|
-
const once = upsertBlock('# AGENTS.md\n\nrules\n', pointerBlock('apps/web'), 'apps/web')
|
|
627
|
-
const again = upsertBlock(once, pointerBlock('apps/web'), 'apps/web')
|
|
628
|
-
assert.equal(again, once)
|
|
629
|
-
const both = upsertBlock(again, pointerBlock('apps/api'), 'apps/api')
|
|
630
|
-
assert.match(both, /apps\/web\/GOLDEN_PATHS\.md/)
|
|
631
|
-
assert.match(both, /apps\/api\/GOLDEN_PATHS\.md/)
|
|
632
|
-
assert.match(both, /^rules$/m)
|
|
633
|
-
})
|
|
634
|
-
|
|
635
|
-
// ---- init / check ----
|
|
636
|
-
|
|
637
|
-
test('init on a fresh repo previews, then writes the starter set', () => {
|
|
638
|
-
const dir = tmpRepo({ 'package.json': JSON.stringify({ scripts: { start: 'next start', build: 'next build' } }), 'pnpm-lock.yaml': '', '.mergify.yml': '' })
|
|
639
|
-
const { plan } = initPlan(path.join(dir, CONTRACT_FILE))
|
|
640
|
-
const preview = applyPlan(plan, { write: false })
|
|
641
|
-
assert.equal(fs.existsSync(path.join(dir, CONTRACT_FILE)), false)
|
|
642
|
-
assert.ok(preview.every((r) => ['create', 'link', 'skip'].includes(r.action)))
|
|
643
|
-
applyPlan(plan, { write: true })
|
|
644
|
-
const c = JSON.parse(fs.readFileSync(path.join(dir, CONTRACT_FILE), 'utf8'))
|
|
645
|
-
assert.equal(c.setup.boot, 'pnpm run start')
|
|
646
|
-
assert.equal(c.ci.queue, 'mergify')
|
|
647
|
-
assert.ok(fs.existsSync(path.join(dir, ENGINE_FILE)))
|
|
648
|
-
assert.ok(fs.existsSync(path.join(dir, ENGINE_FILE.replace('golden-path.mjs', 'golden-path-core.mjs'))))
|
|
649
|
-
assert.ok(fs.existsSync(path.join(dir, VIEW_FILE)))
|
|
650
|
-
assert.equal(fs.existsSync(path.join(dir, WORKFLOW_FILE)), false, 'no workflow until the contract has paths')
|
|
651
|
-
assert.ok(fs.lstatSync(path.join(dir, 'CLAUDE.md')).isSymbolicLink())
|
|
652
|
-
assert.match(fs.readFileSync(path.join(dir, '.gitignore'), 'utf8'), /golden-path-results\.json/)
|
|
653
|
-
})
|
|
654
|
-
|
|
655
|
-
test('init keeps an existing contract and writes the workflow once it validates', () => {
|
|
656
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), 'CLAUDE.md': 'Read AGENTS.md\n' })
|
|
657
|
-
const results = applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
658
|
-
assert.equal(results.find((r) => r.file.endsWith(CONTRACT_FILE)).action, 'skip')
|
|
659
|
-
assert.ok(fs.existsSync(path.join(dir, WORKFLOW_FILE)))
|
|
660
|
-
assert.equal(fs.readFileSync(path.join(dir, 'CLAUDE.md'), 'utf8'), 'Read AGENTS.md\n')
|
|
661
|
-
const again = applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
662
|
-
assert.ok(again.every((r) => r.action === 'unchanged' || r.action === 'skip'), JSON.stringify(again))
|
|
663
|
-
})
|
|
664
|
-
|
|
665
|
-
test('init creates or updates the README link without disturbing surrounding content', async () => {
|
|
666
|
-
const before = '# Product\n\nOverview.\n\n<!-- golden-path:start -->\nOld pointer\n<!-- golden-path:end -->\n\n## Development\n\nRun the app.\n'
|
|
667
|
-
for (const readme of [undefined, before.replace('<!-- golden-path:start -->\nOld pointer\n<!-- golden-path:end -->\n\n', ''), before]) {
|
|
668
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...(readme === undefined ? {} : { 'README.md': readme }) })
|
|
669
|
-
const args = ['init', '--write', '--app-dir', dir]
|
|
670
|
-
assert.equal(await runCli(args, quiet()), 0)
|
|
671
|
-
const file = path.join(dir, 'README.md')
|
|
672
|
-
const generated = fs.readFileSync(file, 'utf8')
|
|
673
|
-
assert.match(generated, /\[GOLDEN_PATHS\.md\]\(GOLDEN_PATHS\.md\)/)
|
|
674
|
-
assert.equal(generated.match(/<!-- golden-path:start -->/g).length, 1)
|
|
675
|
-
if (readme !== undefined) {
|
|
676
|
-
assert.ok(generated.startsWith('# Product\n\nOverview.\n\n'))
|
|
677
|
-
assert.ok(generated.includes('\n## Development\n\nRun the app.\n'))
|
|
678
|
-
if (readme === before) assert.ok(generated.endsWith('\n## Development\n\nRun the app.\n'))
|
|
679
|
-
assert.doesNotMatch(generated, /Old pointer/)
|
|
680
|
-
} else {
|
|
681
|
-
assert.match(generated, /^# README\.md\n/)
|
|
682
|
-
}
|
|
683
|
-
assert.equal(await runCli(args, quiet()), 0)
|
|
684
|
-
assert.equal(fs.readFileSync(file, 'utf8'), generated)
|
|
685
|
-
}
|
|
686
|
-
})
|
|
687
|
-
|
|
688
|
-
test('check reports a hand-edited view and a stale workflow', () => {
|
|
689
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()) })
|
|
690
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
691
|
-
assert.deepEqual(checkDrift(contract(), path.join(dir, CONTRACT_FILE)), [])
|
|
692
|
-
fs.appendFileSync(path.join(dir, VIEW_FILE), 'hand edit\n')
|
|
693
|
-
const c = contract()
|
|
694
|
-
c.ci.queue = 'none'
|
|
695
|
-
const drift = checkDrift(c, path.join(dir, CONTRACT_FILE))
|
|
696
|
-
assert.equal(drift.length, 2)
|
|
697
|
-
})
|
|
698
|
-
|
|
699
|
-
test('the CLI check command exits 1 on drift and 0 when current', async () => {
|
|
700
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
701
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
702
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
703
|
-
assert.equal(await runCli(['check', '--contract', file], quiet()), 0)
|
|
704
|
-
fs.writeFileSync(path.join(dir, VIEW_FILE), 'edited\n')
|
|
705
|
-
assert.equal(await runCli(['check', '--contract', file], quiet()), 1)
|
|
706
|
-
})
|
|
707
|
-
|
|
708
|
-
// ---- migration ----
|
|
709
|
-
|
|
710
|
-
test('migrateLegacy converts a v2 manifest and lists what needs a person', () => {
|
|
711
|
-
const dir = tmpRepo({ 'e2e/a.spec.ts': '' })
|
|
712
|
-
const legacy = {
|
|
713
|
-
profiles: STANDARD_PROFILES,
|
|
714
|
-
setup: { boot: 'pnpm dev on port 3001', env: ['DATABASE_URL'], tenants: [] },
|
|
715
|
-
paths: [
|
|
716
|
-
{ id: 'health', surface: 'api', start: 'GET /api/health', observable: '200 with ok. More detail.', profiles: ['pr-local'] },
|
|
717
|
-
{ id: 'pay', surface: 'browser', journey: '@smoke pay', mutates: true, tenant: 'e2e-pay', profiles: ['pr-local'] },
|
|
718
|
-
{ id: 'docs', surface: 'api', start: 'GET /a, GET /b', observable: 'both 200', profiles: ['pr-local'] },
|
|
719
|
-
],
|
|
720
|
-
}
|
|
721
|
-
const { contract: c, todo } = migrateLegacy(legacy, { appDir: dir })
|
|
722
|
-
assert.equal(c.version, 3)
|
|
723
|
-
assert.equal(c.paths[0].title, '200 with ok.')
|
|
724
|
-
assert.equal(c.paths[1].journey, 'pay')
|
|
725
|
-
assert.deepEqual(c.setup.tenants, ['e2e-pay'])
|
|
726
|
-
assert.deepEqual(c.setup.secrets.fromCi, ['DATABASE_URL'])
|
|
727
|
-
assert.equal(c.setup.env, undefined)
|
|
728
|
-
assert.equal(c.setup.journeys, 'e2e')
|
|
729
|
-
assert.match(c.setup.boot, /^TODO: make this one shell command: pnpm dev on port 3001$/)
|
|
730
|
-
const t = todo.join('\n')
|
|
731
|
-
assert.match(t, /pay.*write cleanup/)
|
|
732
|
-
assert.match(t, /docs.*not one request/)
|
|
733
|
-
assert.match(t, /setup.boot/)
|
|
734
|
-
assert.equal(validateContract(c).ok, false, 'a migrated contract with placeholders must not validate')
|
|
735
|
-
})
|
|
736
|
-
|
|
737
|
-
test('audit finds legacy manifests, forked engines, smoke workflows, and tags', () => {
|
|
738
|
-
const dir = tmpRepo({
|
|
739
|
-
'apps/web/smoke.manifest.json': JSON.stringify({ profiles: { 'pr-local': STANDARD_PROFILES['pr-local'] }, paths: [] }),
|
|
740
|
-
'apps/web/scripts/smoke.mjs': 'const manifest = "smoke.manifest.json"\n'.repeat(40),
|
|
741
|
-
'apps/web/e2e/a.spec.ts': '// @smoke\n// @covers a\n',
|
|
742
|
-
'.github/workflows/smoke.yml': 'on: push\njobs:\n smoke:\n steps:\n - run: node apps/web/scripts/smoke.mjs verdict\n',
|
|
743
|
-
'cloudbuild.yaml': 'steps: [{ args: [node, scripts/smoke.mjs, run] }]\n',
|
|
744
|
-
})
|
|
745
|
-
const a = audit(dir)
|
|
746
|
-
assert.deepEqual(a.legacy, ['apps/web/smoke.manifest.json'])
|
|
747
|
-
assert.equal(a.smokeTags, 1)
|
|
748
|
-
const kinds = a.findings.map((f) => `${f.kind}:${f.file}`)
|
|
749
|
-
assert.ok(kinds.includes('engine:apps/web/scripts/smoke.mjs'))
|
|
750
|
-
assert.ok(kinds.includes('drift:apps/web/smoke.manifest.json'), 'pr-local declared, nothing runs on pull_request')
|
|
751
|
-
assert.match(a.findings.find((f) => f.file === 'cloudbuild.yaml').note, /deployed profile/)
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
test('audit --write migrates beside the legacy manifest and keeps it unless asked', async () => {
|
|
755
|
-
const legacy = { profiles: STANDARD_PROFILES, setup: { boot: 'pnpm start' }, paths: [{ id: 'health', surface: 'api', start: 'GET /h', observable: 'ok', profiles: ['pr-local'] }] }
|
|
756
|
-
const dir = tmpRepo({ 'apps/web/smoke.manifest.json': JSON.stringify(legacy) })
|
|
757
|
-
const q = quiet()
|
|
758
|
-
assert.equal(await runCli(['audit', '--write', '--repo-root', dir], q), 0)
|
|
759
|
-
assert.ok(fs.existsSync(path.join(dir, 'apps/web', CONTRACT_FILE)))
|
|
760
|
-
assert.ok(fs.existsSync(path.join(dir, 'apps/web/smoke.manifest.json')))
|
|
761
|
-
assert.ok(fs.existsSync(path.join(dir, 'apps/web', ENGINE_FILE)))
|
|
762
|
-
assert.match(fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8'), /apps\/web\/GOLDEN_PATHS\.md/)
|
|
763
|
-
assert.match(q.out.join('\n'), /needs a person/)
|
|
764
|
-
})
|
|
765
|
-
|
|
766
|
-
// ---- run ----
|
|
767
|
-
|
|
768
|
-
test('run drives api and cli paths and the verdict follows', async () => {
|
|
769
|
-
const server = http.createServer((req, res) => {
|
|
770
|
-
if (req.url === '/api/health') { res.writeHead(200, { 'content-type': 'application/json' }); res.end('{"status":"ok"}') }
|
|
771
|
-
else { res.writeHead(500); res.end('boom') }
|
|
772
|
-
})
|
|
773
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', r))
|
|
774
|
-
const target = `http://127.0.0.1:${server.address().port}`
|
|
775
|
-
try {
|
|
776
|
-
const c = contract({ paths: [
|
|
777
|
-
{ id: 'health', title: 'up', surface: 'api', start: 'GET /api/health', observable: '200', expect: { status: 200, json: { status: 'ok' } }, profiles: ['pr-local'] },
|
|
778
|
-
{ id: 'orders', title: 'orders', surface: 'api', request: { method: 'GET', path: '/api/orders' }, observable: '200', profiles: ['pr-local'] },
|
|
779
|
-
{ id: 'migrate', title: 'migrations', surface: 'cli', command: 'echo migrated', observable: 'prints migrated', expect: { stdoutIncludes: 'migrated' }, profiles: ['pr-local'] },
|
|
780
|
-
{ id: 'docs', title: 'docs', surface: 'api', start: 'GET /a, GET /b', observable: 'both', profiles: ['pr-local'] },
|
|
781
|
-
] })
|
|
782
|
-
const results = await runPaths(c, { profile: 'pr-local', target, appDir: os.tmpdir() })
|
|
783
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['health', 'pass'], ['orders', 'fail'], ['migrate', 'pass'], ['docs', 'cannot-run']])
|
|
784
|
-
const { run } = selectPathsForProfile(c, 'pr-local')
|
|
785
|
-
assert.equal(computeVerdict(run, results).verdict, 'DEAD')
|
|
786
|
-
} finally {
|
|
787
|
-
server.close()
|
|
788
|
-
}
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
test('run with --expect-sha refuses a target serving another commit', async () => {
|
|
792
|
-
const server = http.createServer((req, res) => { res.writeHead(200); res.end('{"build":{"sha":"abc1234"}}') })
|
|
793
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', r))
|
|
794
|
-
const target = `http://127.0.0.1:${server.address().port}`
|
|
795
|
-
try {
|
|
796
|
-
const c = contract()
|
|
797
|
-
c.setup.attestation = { path: '/version', field: 'build.sha' }
|
|
798
|
-
const wrong = await runPaths(c, { profile: 'pr-local', target, appDir: os.tmpdir(), expectSha: 'def5678' })
|
|
799
|
-
assert.ok(wrong.every((r) => r.status === 'cannot-run' && /serves abc1234/.test(r.note)))
|
|
800
|
-
} finally {
|
|
801
|
-
server.close()
|
|
802
|
-
}
|
|
803
|
-
})
|
|
804
|
-
|
|
805
|
-
test('the verdict command exits 0 only on ALIVE and writes the verdict file', async () => {
|
|
806
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
807
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
808
|
-
const results = path.join(dir, 'r.json')
|
|
809
|
-
const bound = (r) => JSON.stringify(resultsEnvelope(r, { profile: 'prod-preview', target: 'http://127.0.0.1:3000' }))
|
|
810
|
-
fs.writeFileSync(results, bound([{ id: 'health', status: 'pass' }, { id: 'sign-in', status: 'pass' }]))
|
|
811
|
-
const out = path.join(dir, 'v.json')
|
|
812
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'prod-preview', '--results', results, '--out', out], quiet()), 0)
|
|
813
|
-
assert.equal(JSON.parse(fs.readFileSync(out, 'utf8')).noMutations, true)
|
|
814
|
-
fs.writeFileSync(results, bound([{ id: 'health', status: 'fail' }, { id: 'sign-in', status: 'pass' }]))
|
|
815
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'prod-preview', '--results', results, '--out', out], quiet()), 1)
|
|
816
|
-
})
|
|
817
|
-
|
|
818
|
-
test('the CLI refuses unknown flags and a missing contract', async () => {
|
|
819
|
-
assert.equal(await runCli(['validate', '--no-mutations'], quiet()), 2)
|
|
820
|
-
const dir = tmpRepo({ 'smoke.manifest.json': '{}' })
|
|
821
|
-
const q = quiet()
|
|
822
|
-
assert.equal(await runCli(['validate', '--contract', path.join(dir, CONTRACT_FILE)], q), 2)
|
|
823
|
-
assert.match(q.out.join('\n'), /audit --write/)
|
|
824
|
-
})
|
|
825
|
-
|
|
826
|
-
test('run maps a Playwright JSON report back to each browser path', async () => {
|
|
827
|
-
// A stand-in for `playwright test`: it receives the spec files and writes the
|
|
828
|
-
// JSON report where the engine asked for it, failing checkout.spec.ts.
|
|
829
|
-
const fake = [
|
|
830
|
-
"const fs = require('node:fs')",
|
|
831
|
-
"const files = process.argv.slice(2).filter((a) => a.endsWith('.spec.ts'))",
|
|
832
|
-
"const ok = { status: 'expected', results: [{ status: 'passed' }] }",
|
|
833
|
-
"const bad = { status: 'unexpected', results: [{ status: 'failed' }] }",
|
|
834
|
-
"const suites = files.map((f) => ({ file: f.replace(/^e2e\\//, ''), specs: [{ tests: [f.includes('checkout') ? bad : ok] }] }))",
|
|
835
|
-
"fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: require('node:path').resolve('e2e') }, suites }))",
|
|
836
|
-
"process.exitCode = suites.some((x) => x.file.includes('checkout')) ? 1 : 0",
|
|
837
|
-
].join('\n')
|
|
838
|
-
const dir = tmpRepo({ ...SPECS, 'fake-pw.cjs': fake })
|
|
839
|
-
const c = contract()
|
|
840
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
841
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
842
|
-
const results = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
843
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['sign-in', 'pass'], ['checkout', 'fail']])
|
|
844
|
-
})
|
|
845
|
-
|
|
846
|
-
test('run reports a browser path whose spec is missing as cannot-run', async () => {
|
|
847
|
-
const dir = tmpRepo({ 'e2e/sign-in.spec.ts': SPECS['e2e/sign-in.spec.ts'], 'fake-pw.cjs': "require('node:fs').writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: require('node:path').resolve('e2e') }, suites: [{ file: 'sign-in.spec.ts', specs: [{ tests: [{ status: 'expected', results: [{ status: 'passed' }] }] }] }] }))" })
|
|
848
|
-
const c = contract()
|
|
849
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
850
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
851
|
-
const results = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
852
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['sign-in', 'pass'], ['checkout', 'cannot-run']])
|
|
853
|
-
})
|
|
854
|
-
|
|
855
|
-
test('the shipped example contract validates', () => {
|
|
856
|
-
const file = new URL('../assets/golden-paths.example.json', import.meta.url)
|
|
857
|
-
assert.deepEqual(validateContract(JSON.parse(fs.readFileSync(file, 'utf8'))).errors, [])
|
|
858
|
-
})
|
|
859
|
-
|
|
860
|
-
test('the copied engine runs from the consumer repo', async () => {
|
|
861
|
-
const { spawnSync } = await import('node:child_process')
|
|
862
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
863
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
864
|
-
const r = spawnSync(process.execPath, [ENGINE_FILE, 'check'], { cwd: dir, encoding: 'utf8' })
|
|
865
|
-
assert.equal(r.status, 0, r.stdout + r.stderr)
|
|
866
|
-
assert.match(r.stdout, /generated files current/)
|
|
867
|
-
assert.equal(spawnSync(process.execPath, [ENGINE_FILE, 'version'], { cwd: dir, encoding: 'utf8' }).stdout.trim(), engineIdentity())
|
|
868
|
-
assert.ok(engineIdentity().startsWith(`${ENGINE_VERSION}+`))
|
|
869
|
-
})
|
|
870
|
-
|
|
871
|
-
test('verdict refuses results observed under another profile or target', () => {
|
|
872
|
-
const env = resultsEnvelope([{ id: 'health', status: 'pass' }], { profile: 'pr-local', target: 'http://127.0.0.1:3000' })
|
|
873
|
-
assert.match(openResults(env, { profile: 'prod-preview' }).error, /observed under profile "pr-local"/)
|
|
874
|
-
assert.match(openResults(env, { profile: 'pr-local', target: 'https://prod' }).error, /observed against http:\/\/127\.0\.0\.1:3000/)
|
|
875
|
-
assert.deepEqual(openResults(env, { profile: 'pr-local' }).results, env.results)
|
|
876
|
-
assert.equal(openResults([{ id: 'x', status: 'pass' }], { profile: 'any' }).results.length, 1)
|
|
877
|
-
})
|
|
878
|
-
|
|
879
|
-
test('the CLI cannot turn a local run into a production ALIVE', async () => {
|
|
880
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
881
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
882
|
-
const results = path.join(dir, 'r.json')
|
|
883
|
-
fs.writeFileSync(results, JSON.stringify(resultsEnvelope([{ id: 'health', status: 'pass' }, { id: 'sign-in', status: 'pass' }], { profile: 'pr-local', target: 'http://127.0.0.1:3000' })))
|
|
884
|
-
const q = quiet()
|
|
885
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'prod-preview', '--results', results], q), 1)
|
|
886
|
-
assert.match(q.out.join('\n'), /observed under profile "pr-local"/)
|
|
887
|
-
})
|
|
888
|
-
|
|
889
|
-
test('a repo without a lockfile gets no dependency cache and a plain install', () => {
|
|
890
|
-
const dir = tmpRepo({ 'package.json': JSON.stringify({ scripts: { start: 'node server.mjs' } }) })
|
|
891
|
-
const { plan } = initPlan(path.join(dir, CONTRACT_FILE))
|
|
892
|
-
const c = JSON.parse(plan.find((s) => s.file.endsWith(CONTRACT_FILE)).next)
|
|
893
|
-
assert.equal(c.ci.cache, false)
|
|
894
|
-
c.paths = contract().paths.slice(0, 1)
|
|
895
|
-
c.setup.tenants = []
|
|
896
|
-
const wf = renderWorkflow(c)
|
|
897
|
-
assert.doesNotMatch(wf, /cache: npm/)
|
|
898
|
-
assert.match(wf, /run: "npm install"/)
|
|
899
|
-
})
|
|
900
|
-
|
|
901
|
-
test('the workflow waits on the contract health path when healthPath is unset', () => {
|
|
902
|
-
const c = contract()
|
|
903
|
-
delete c.setup.healthPath
|
|
904
|
-
assert.equal(inferHealthPath(c), '/api/health')
|
|
905
|
-
assert.match(renderWorkflow(c), /wait --url "http:\/\/127\.0\.0\.1:3000\/api\/health"/)
|
|
906
|
-
})
|
|
907
|
-
|
|
908
|
-
test('the view names the workflow only once the contract generates one', () => {
|
|
909
|
-
assert.doesNotMatch(renderView(contract({ paths: [] })), /golden-paths\.yml/)
|
|
910
|
-
assert.match(renderView(contract()), /golden-paths\.yml/)
|
|
911
|
-
assert.match(renderView(contract()), /init --write/)
|
|
912
|
-
assert.doesNotMatch(renderView(contract()), /render --write/)
|
|
913
|
-
})
|
|
914
|
-
|
|
915
|
-
test('--help prints usage and exits 0', async () => {
|
|
916
|
-
const q = quiet()
|
|
917
|
-
assert.equal(await runCli(['--help'], q), 0)
|
|
918
|
-
assert.equal(q.out[0], USAGE)
|
|
919
|
-
})
|
|
920
|
-
|
|
921
|
-
test('--surfaces narrows selection, and the verdict must use the same narrowing', () => {
|
|
922
|
-
const sel = selectPathsForProfile(contract(), 'pr-local', { surfaces: ['api'] })
|
|
923
|
-
assert.deepEqual(sel.run.map((p) => p.id), ['health'])
|
|
924
|
-
assert.match(sel.excluded.find((x) => x.id === 'sign-in').reason, /surface browser not selected/)
|
|
925
|
-
assert.throws(() => selectPathsForProfile(contract(), 'pr-local', { surfaces: ['web'] }), /unknown surface web/)
|
|
926
|
-
const env = resultsEnvelope([{ id: 'health', status: 'pass' }], { profile: 'pr-local', target: 't', surfaces: ['api'] })
|
|
927
|
-
assert.deepEqual(openResults(env, { profile: 'pr-local' }).surfaces, ['api'])
|
|
928
|
-
assert.match(openResults(env, { profile: 'pr-local', surfaces: [] }).error, /--surfaces api/)
|
|
929
|
-
})
|
|
930
|
-
|
|
931
|
-
test('the CLI verdict reuses the run\'s surface narrowing', async () => {
|
|
932
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
933
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
934
|
-
const results = path.join(dir, 'r.json')
|
|
935
|
-
fs.writeFileSync(results, JSON.stringify(resultsEnvelope([{ id: 'health', status: 'pass' }], { profile: 'prod-preview', target: 'https://c', surfaces: ['api'] })))
|
|
936
|
-
const out = path.join(dir, 'v.json')
|
|
937
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'prod-preview', '--results', results, '--out', out], quiet()), 0)
|
|
938
|
-
assert.deepEqual(JSON.parse(fs.readFileSync(out, 'utf8')).surfaces, ['api'])
|
|
939
|
-
})
|
|
940
|
-
|
|
941
|
-
test('workflows are classified by commands and triggers, not by mentions', () => {
|
|
942
|
-
assert.equal(classifyWorkflow('.github/workflows/e2e.yml', 'on:\n workflow_dispatch:\njobs:\n e2e:\n # smoke lanes live elsewhere\n steps: [{ run: pnpm test:e2e }]\n'), null)
|
|
943
|
-
assert.deepEqual(classifyWorkflow('.github/workflows/smoke.yml', 'on:\n push:\n branches:\n - main\n\n - "mergify/merge-queue/**"\njobs:\n s:\n steps:\n - run: node scripts/smoke.mjs verdict --profile queue-local\n'), { file: '.github/workflows/smoke.yml', lane: 'queue', gate: true })
|
|
944
|
-
assert.deepEqual(classifyWorkflow('.github/workflows/ci.yml', 'on: [pull_request]\njobs:\n v:\n steps:\n - run: node scripts/smoke.mjs validate\n'), { file: '.github/workflows/ci.yml', lane: 'pr', gate: false })
|
|
945
|
-
assert.equal(classifyWorkflow('cloudbuild.yaml', 'steps:\n- args: [node, scripts/smoke.mjs, run, --profile, dev-active]\n').lane, 'deploy')
|
|
946
|
-
assert.equal(classifyWorkflow('.github/workflows/manual.yml', 'on:\n workflow_dispatch:\njobs:\n s:\n steps:\n - run: pnpm smoke:run\n').lane, 'manual')
|
|
947
|
-
})
|
|
948
|
-
|
|
949
|
-
test('audit ignores a smoke-named one-off probe that is not a gate', () => {
|
|
950
|
-
const dir = tmpRepo({
|
|
951
|
-
'scripts/536-runtime-smoke.mjs': 'console.log("probe")\n',
|
|
952
|
-
'scripts/smoke.mjs': 'const m = "smoke.manifest.json"\n',
|
|
953
|
-
})
|
|
954
|
-
const files = audit(dir).findings.filter((f) => f.kind === 'engine').map((f) => f.file)
|
|
955
|
-
assert.deepEqual(files, ['scripts/smoke.mjs'])
|
|
956
|
-
})
|
|
957
|
-
|
|
958
|
-
test('specTitles strips tags and only a single-test spec lends its name in migration', () => {
|
|
959
|
-
const dir = tmpRepo({
|
|
960
|
-
'e2e/one.spec.ts': "// @smoke\n// @covers one\ntest('@smoke one: user signs in', () => {})\n",
|
|
961
|
-
'e2e/two.spec.ts': "// @smoke\n// @covers two\ntest('first case', () => {})\ntest('second case', () => {})\n",
|
|
962
|
-
})
|
|
963
|
-
assert.deepEqual(specTitles([path.join(dir, 'e2e/one.spec.ts')]), ['User signs in'])
|
|
964
|
-
const legacy = { profiles: STANDARD_PROFILES, setup: { boot: 'x', journeys: 'e2e' }, paths: [
|
|
965
|
-
{ id: 'one', surface: 'browser', journey: '@smoke one', profiles: ['pr-local'] },
|
|
966
|
-
{ id: 'two', surface: 'browser', journey: '@smoke two', profiles: ['pr-local'] },
|
|
967
|
-
] }
|
|
968
|
-
const { contract: c } = migrateLegacy(legacy, { appDir: dir })
|
|
969
|
-
assert.equal(c.paths[0].title, 'User signs in')
|
|
970
|
-
assert.match(c.paths[1].title, /^TODO/)
|
|
971
|
-
})
|
|
972
|
-
|
|
973
|
-
// ---- regressions from the independent audit ----
|
|
974
|
-
|
|
975
|
-
function fakePlaywright(body) {
|
|
976
|
-
return `const fs = require('node:fs'); const path = require('node:path');\n${body}`
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
test('a spec covering two journeys is refused, so read-only runs never execute a writing journey', async () => {
|
|
980
|
-
const dir = tmpRepo({ 'e2e/shared.spec.ts': '// @golden-path\n// @covers sign-in\n// @covers checkout\n', 'fake-pw.cjs': fakePlaywright("fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, '{}'); fs.writeFileSync('ran.txt', 'yes')") })
|
|
981
|
-
const c = contract()
|
|
982
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
983
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
984
|
-
assert.match(validateContractAt(c, dir).join('\n'), /covers sign-in, checkout; keep one golden-path journey per spec file/)
|
|
985
|
-
const results = await runPaths(c, { profile: 'prod-preview', target: 'http://127.0.0.1:9', appDir: dir })
|
|
986
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['sign-in', 'cannot-run']])
|
|
987
|
-
assert.equal(fs.existsSync(path.join(dir, 'ran.txt')), false, 'the shared spec must not be executed')
|
|
988
|
-
})
|
|
989
|
-
|
|
990
|
-
function validateContractAt(c, dir) {
|
|
991
|
-
return reconcileJourneys(c, [...scanJourneyTags(path.join(dir, 'e2e')).byId.keys()]).errors.concat(
|
|
992
|
-
[...scanJourneyTags(path.join(dir, 'e2e')).byFile].filter(([, ids]) => ids.length > 1).map(([f, ids]) => `tagged spec ${f} covers ${ids.join(', ')}; keep one golden-path journey per spec file`))
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
test('a non-boolean mutates is invalid and is treated as writing', () => {
|
|
996
|
-
const c = contract()
|
|
997
|
-
c.paths[0] = { ...c.paths[0], mutates: 'true' }
|
|
998
|
-
assert.match(validateContract(c).errors.join('\n'), /mutates must be true or false/)
|
|
999
|
-
assert.deepEqual(selectPathsForProfile(c, 'prod-preview').run.map((p) => p.id), ['sign-in'])
|
|
1000
|
-
})
|
|
1001
|
-
|
|
1002
|
-
test('two specs with the same name are never confused', async () => {
|
|
1003
|
-
const dir = tmpRepo({
|
|
1004
|
-
'e2e/login.spec.ts': '// @golden-path\n// @covers sign-in\n',
|
|
1005
|
-
'e2e/admin/login.spec.ts': '// @golden-path\n// @covers checkout\n',
|
|
1006
|
-
'fake-pw.cjs': fakePlaywright(`
|
|
1007
|
-
const ok = { status: 'expected', results: [{ status: 'passed' }] }
|
|
1008
|
-
const bad = { status: 'unexpected', results: [{ status: 'failed' }] }
|
|
1009
|
-
fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: path.resolve('e2e') }, suites: [
|
|
1010
|
-
{ file: 'login.spec.ts', specs: [{ tests: [ok] }] },
|
|
1011
|
-
{ file: 'admin/login.spec.ts', specs: [{ tests: [bad] }] },
|
|
1012
|
-
] }))
|
|
1013
|
-
process.exitCode = 1`),
|
|
1014
|
-
})
|
|
1015
|
-
const c = contract()
|
|
1016
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
1017
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
1018
|
-
const results = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
1019
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['sign-in', 'pass'], ['checkout', 'fail']])
|
|
1020
|
-
})
|
|
1021
|
-
|
|
1022
|
-
test('a broken browser run turns its passes into cannot-run', async () => {
|
|
1023
|
-
const dir = tmpRepo({ ...SPECS, 'fake-pw.cjs': fakePlaywright(`
|
|
1024
|
-
const ok = { status: 'expected', results: [{ status: 'passed' }] }
|
|
1025
|
-
fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: path.resolve('e2e') }, errors: [{ message: 'global teardown failed' }], suites: [
|
|
1026
|
-
{ file: 'sign-in.spec.ts', specs: [{ tests: [ok] }] }, { file: 'checkout.spec.ts', specs: [{ tests: [ok] }] },
|
|
1027
|
-
] }))
|
|
1028
|
-
process.exitCode = 1`) })
|
|
1029
|
-
const c = contract()
|
|
1030
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
1031
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
1032
|
-
const results = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
1033
|
-
assert.ok(results.every((r) => r.status === 'cannot-run' && /global teardown failed/.test(r.note)), JSON.stringify(results))
|
|
1034
|
-
})
|
|
1035
|
-
|
|
1036
|
-
test('a request path cannot leave the target origin', async () => {
|
|
1037
|
-
const c = contract({ paths: [{ id: 'h', title: 'h', surface: 'api', request: { path: '//other.example/health' }, observable: '200', profiles: ['pr-local'] }] })
|
|
1038
|
-
assert.match(validateContract(c).errors.join('\n'), /single \//)
|
|
1039
|
-
const [r] = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: os.tmpdir() })
|
|
1040
|
-
assert.equal(r.status, 'cannot-run')
|
|
1041
|
-
assert.match(r.note, /leaves the target origin/)
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
test('attestation needs a real commit sha on both sides', async () => {
|
|
1045
|
-
const server = http.createServer((req, res) => { res.writeHead(200); res.end('{"sha":"a"}') })
|
|
1046
|
-
await new Promise((r) => server.listen(0, '127.0.0.1', r))
|
|
1047
|
-
try {
|
|
1048
|
-
const c = contract()
|
|
1049
|
-
c.setup.attestation = { path: '/version', field: 'sha' }
|
|
1050
|
-
const target = `http://127.0.0.1:${server.address().port}`
|
|
1051
|
-
const results = await runPaths(c, { profile: 'pr-local', target, appDir: os.tmpdir(), expectSha: 'abcdef0123456789' })
|
|
1052
|
-
assert.ok(results.every((r) => r.status === 'cannot-run' && /not a commit sha/.test(r.note)))
|
|
1053
|
-
const short = await runPaths(c, { profile: 'pr-local', target, appDir: os.tmpdir(), expectSha: 'a' })
|
|
1054
|
-
assert.ok(short.every((r) => /at least 7 hex/.test(r.note)))
|
|
1055
|
-
} finally {
|
|
1056
|
-
server.close()
|
|
1057
|
-
}
|
|
1058
|
-
})
|
|
1059
|
-
|
|
1060
|
-
test('stdoutIncludes reads stdout only', async () => {
|
|
1061
|
-
const c = contract({ paths: [{ id: 'cli', title: 'cli', surface: 'cli', command: 'printf OK >&2', observable: 'ok', expect: { stdoutIncludes: 'OK' }, profiles: ['pr-local'] }] })
|
|
1062
|
-
const [r] = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: os.tmpdir() })
|
|
1063
|
-
assert.equal(r.status, 'fail')
|
|
1064
|
-
assert.match(r.note, /it is on stderr/)
|
|
1065
|
-
})
|
|
1066
|
-
|
|
1067
|
-
test('contract values cannot inject shell or YAML into the workflow', () => {
|
|
1068
|
-
const bad = [
|
|
1069
|
-
(c) => { c.setup.baseUrl = 'http://localhost/$(printf X)/' },
|
|
1070
|
-
(c) => { c.setup.secrets = { generate: ['AUTH$(printf X)'] } },
|
|
1071
|
-
(c) => { c.ci.runner = 'ubuntu-latest\n container: attacker/image' },
|
|
1072
|
-
(c) => { c.setup.env = { BASE_URL: 'https://prod' } },
|
|
1073
|
-
(c) => { c.setup.env = { GOLDEN_PATH_PROFILE: 'prod-preview' } },
|
|
1074
|
-
(c) => { c.setup.database = { engine: 'postgres' }; c.setup.secrets = { fromCi: ['DATABASE_URL'] } },
|
|
1075
|
-
(c) => { c.setup.healthPath = '//evil' },
|
|
1076
|
-
]
|
|
1077
|
-
for (const mutate of bad) {
|
|
1078
|
-
const c = contract()
|
|
1079
|
-
mutate(c)
|
|
1080
|
-
assert.equal(validateContract(c).ok, false, JSON.stringify(c.setup) + JSON.stringify(c.ci))
|
|
1081
|
-
}
|
|
1082
|
-
const ok = contract()
|
|
1083
|
-
ok.ci.runner = '[self-hosted, linux]'
|
|
1084
|
-
assert.equal(validateContract(ok).ok, true)
|
|
1085
|
-
})
|
|
1086
|
-
|
|
1087
|
-
test('two apps in one monorepo keep separate workflows and pointers', () => {
|
|
1088
|
-
const dir = tmpRepo({ 'apps/a/golden-paths.json': JSON.stringify(contract()), 'apps/b/golden-paths.json': JSON.stringify(contract()), ...Object.fromEntries(Object.entries(SPECS).flatMap(([f, t]) => [[`apps/a/${f}`, t], [`apps/b/${f}`, t]])) })
|
|
1089
|
-
applyPlan(initPlan(path.join(dir, 'apps/a', CONTRACT_FILE)).plan, { write: true })
|
|
1090
|
-
applyPlan(initPlan(path.join(dir, 'apps/b', CONTRACT_FILE)).plan, { write: true })
|
|
1091
|
-
assert.deepEqual(checkDrift(contract(), path.join(dir, 'apps/a', CONTRACT_FILE)), [])
|
|
1092
|
-
assert.deepEqual(checkDrift(contract(), path.join(dir, 'apps/b', CONTRACT_FILE)), [])
|
|
1093
|
-
assert.ok(fs.existsSync(path.join(dir, workflowFile('apps/a'))) && fs.existsSync(path.join(dir, workflowFile('apps/b'))))
|
|
1094
|
-
assert.match(workflowFile('apps/a'), /^\.github\/workflows\/golden-paths-apps-a-[0-9a-f]{6}\.yml$/)
|
|
1095
|
-
assert.notEqual(workflowFile('apps/a-b'), workflowFile('apps/a/b'))
|
|
1096
|
-
const agents = fs.readFileSync(path.join(dir, 'AGENTS.md'), 'utf8')
|
|
1097
|
-
assert.match(agents, /apps\/a\/GOLDEN_PATHS\.md/)
|
|
1098
|
-
assert.match(agents, /apps\/b\/GOLDEN_PATHS\.md/)
|
|
1099
|
-
const readmeFile = path.join(dir, 'README.md')
|
|
1100
|
-
const readme = fs.readFileSync(readmeFile, 'utf8')
|
|
1101
|
-
for (const app of ['a', 'b']) {
|
|
1102
|
-
assert.ok(readme.includes(`[apps/${app}/GOLDEN_PATHS.md](apps/${app}/GOLDEN_PATHS.md)`))
|
|
1103
|
-
assert.ok(fs.existsSync(path.join(dir, `apps/${app}/GOLDEN_PATHS.md`)))
|
|
1104
|
-
applyPlan(initPlan(path.join(dir, `apps/${app}`, CONTRACT_FILE)).plan, { write: true })
|
|
1105
|
-
}
|
|
1106
|
-
assert.equal(fs.readFileSync(readmeFile, 'utf8'), readme)
|
|
1107
|
-
})
|
|
1108
|
-
|
|
1109
|
-
test('disabling CI removes the generated workflow on the next init', () => {
|
|
1110
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
1111
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
1112
|
-
applyPlan(initPlan(file).plan, { write: true })
|
|
1113
|
-
const off = contract({ ci: { enabled: false } })
|
|
1114
|
-
fs.writeFileSync(file, JSON.stringify(off))
|
|
1115
|
-
const results = applyPlan(initPlan(file).plan, { write: true })
|
|
1116
|
-
assert.ok(results.some((r) => r.action === 'delete'))
|
|
1117
|
-
assert.equal(fs.existsSync(path.join(dir, WORKFLOW_FILE)), false)
|
|
1118
|
-
assert.deepEqual(checkDrift(off, file), [])
|
|
1119
|
-
})
|
|
1120
|
-
|
|
1121
|
-
test('init never deletes a hand-written workflow of the same name', () => {
|
|
1122
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract({ ci: { enabled: false } })), ...SPECS, [WORKFLOW_FILE]: 'name: mine\n' })
|
|
1123
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
1124
|
-
assert.equal(fs.readFileSync(path.join(dir, WORKFLOW_FILE), 'utf8'), 'name: mine\n')
|
|
1125
|
-
})
|
|
1126
|
-
|
|
1127
|
-
test('a monorepo app caches on the workspace-root lockfile', () => {
|
|
1128
|
-
const dir = tmpRepo({ 'pnpm-lock.yaml': '', 'apps/web/package.json': JSON.stringify({ scripts: { start: 'next start' } }) })
|
|
1129
|
-
const { plan } = initPlan(path.join(dir, 'apps/web', CONTRACT_FILE))
|
|
1130
|
-
const c = JSON.parse(plan.find((x) => x.file.endsWith(CONTRACT_FILE)).next)
|
|
1131
|
-
assert.equal(c.ci.lockfile, 'pnpm-lock.yaml')
|
|
1132
|
-
c.paths = contract().paths.slice(0, 1)
|
|
1133
|
-
assert.match(renderWorkflow(c, { appRel: 'apps/web' }), /cache-dependency-path: "pnpm-lock\.yaml"$/m)
|
|
1134
|
-
})
|
|
1135
|
-
|
|
1136
|
-
test('migration keeps unknown top-level fields', () => {
|
|
1137
|
-
const legacy = { profiles: STANDARD_PROFILES, criticalMetadata: { owner: 'payments' }, setup: { boot: 'x' }, paths: [{ id: 'h', surface: 'api', start: 'GET /h', observable: 'ok', profiles: ['pr-local'], evidenceClass: 'local-app' }] }
|
|
1138
|
-
const { contract: c } = migrateLegacy(legacy, { appDir: tmpRepo() })
|
|
1139
|
-
assert.deepEqual(c.criticalMetadata, { owner: 'payments' })
|
|
1140
|
-
assert.equal(c.paths[0].evidenceClass, 'local-app')
|
|
1141
|
-
})
|
|
1142
|
-
|
|
1143
|
-
test('audit skips the generated workflows', () => {
|
|
1144
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
1145
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
1146
|
-
assert.deepEqual(audit(dir).workflows, [])
|
|
1147
|
-
})
|
|
1148
|
-
|
|
1149
|
-
async function freePort() {
|
|
1150
|
-
const s = http.createServer()
|
|
1151
|
-
await new Promise((r) => s.listen(0, '127.0.0.1', r))
|
|
1152
|
-
const { port } = s.address()
|
|
1153
|
-
await new Promise((r) => s.close(r))
|
|
1154
|
-
return port
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
function bootContract(port, boot) {
|
|
1158
|
-
return contract({
|
|
1159
|
-
setup: { boot, baseUrl: `http://127.0.0.1:${port}`, env: { PORT: String(port) }, tenants: ['e2e-synthetic'] },
|
|
1160
|
-
paths: [{ id: 'health', title: 'up', surface: 'api', start: 'GET /api/health', observable: '200', expect: { status: 200 }, profiles: ['pr-local', 'prod-preview'] }],
|
|
1161
|
-
})
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
const SERVER = "require('node:http').createServer((q, s) => { s.writeHead(q.url === '/api/health' ? 200 : 404); s.end('ok') }).listen(Number(process.env.PORT), '127.0.0.1')"
|
|
1165
|
-
|
|
1166
|
-
test('run --boot starts the app, drives it, and stops it', async () => {
|
|
1167
|
-
const port = await freePort()
|
|
1168
|
-
const dir = tmpRepo({ 'server.cjs': SERVER, [CONTRACT_FILE]: JSON.stringify(bootContract(port, 'node server.cjs')) })
|
|
1169
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
1170
|
-
assert.equal(await runCli(['run', '--contract', file, '--profile', 'pr-local', '--boot', '--timeout', '20'], quiet()), 0)
|
|
1171
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'pr-local'], quiet()), 0)
|
|
1172
|
-
const down = await fetch(`http://127.0.0.1:${port}/api/health`).then(() => false, () => true)
|
|
1173
|
-
assert.ok(down, 'the booted app must be stopped after the run')
|
|
1174
|
-
})
|
|
1175
|
-
|
|
1176
|
-
test('run --boot refuses a deployed profile and reports an app that never came up', async () => {
|
|
1177
|
-
const port = await freePort()
|
|
1178
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(bootContract(port, 'exit 3')) })
|
|
1179
|
-
const file = path.join(dir, CONTRACT_FILE)
|
|
1180
|
-
assert.equal(await runCli(['run', '--contract', file, '--profile', 'prod-preview', '--boot'], quiet()), 2)
|
|
1181
|
-
const q = quiet()
|
|
1182
|
-
assert.equal(await runCli(['run', '--contract', file, '--profile', 'pr-local', '--boot', '--timeout', '2'], q), 0)
|
|
1183
|
-
assert.match(q.out.join('\n'), /did not come up/)
|
|
1184
|
-
assert.equal(await runCli(['verdict', '--contract', file, '--profile', 'pr-local'], quiet()), 1)
|
|
1185
|
-
})
|
|
1186
|
-
|
|
1187
|
-
test('e2e-test surface ids in @covers are not golden-path journeys', () => {
|
|
1188
|
-
const dir = tmpRepo({ 'e2e/cases.spec.ts': '// @golden-path\n// @covers /cases/[id]\n// @covers api:GET /cases\n// @covers case-pool-to-workbench\n' })
|
|
1189
|
-
const { byId, byFile } = scanJourneyTags(path.join(dir, 'e2e'))
|
|
1190
|
-
assert.deepEqual([...byId.keys()], ['case-pool-to-workbench'])
|
|
1191
|
-
assert.deepEqual([...byFile.values()], [['case-pool-to-workbench']])
|
|
1192
|
-
})
|
|
1193
|
-
|
|
1194
|
-
test('version 3 rejects an api start that is not one request', () => {
|
|
1195
|
-
const c = contract()
|
|
1196
|
-
c.paths[0].start = 'GET ${REMOTE_URL}/graph'
|
|
1197
|
-
assert.match(validateContract(c).errors.join('\n'), /is not one "METHOD \/path" request/)
|
|
1198
|
-
})
|
|
1199
|
-
|
|
1200
|
-
test('a browser path with its own Playwright config runs in its own invocation', async () => {
|
|
1201
|
-
const dir = tmpRepo({ ...SPECS, 'fake-pw.cjs': fakePlaywright(`
|
|
1202
|
-
const files = process.argv.slice(2).filter((a) => a.endsWith('.spec.ts'))
|
|
1203
|
-
fs.appendFileSync('calls.txt', process.argv.slice(2).join(' ') + '\\n')
|
|
1204
|
-
const ok = { status: 'expected', results: [{ status: 'passed' }] }
|
|
1205
|
-
fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: path.resolve('e2e') }, suites: files.map((f) => ({ file: f.replace(/^e2e\\//, ''), specs: [{ tests: [ok] }] })) }))`) })
|
|
1206
|
-
const c = contract()
|
|
1207
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
1208
|
-
c.paths = c.paths.filter((p) => p.surface === 'browser')
|
|
1209
|
-
c.paths[1].config = 'playwright.identity.config.ts'
|
|
1210
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
1211
|
-
const results = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
1212
|
-
assert.deepEqual(results.map((r) => [r.id, r.status]), [['sign-in', 'pass'], ['checkout', 'pass']])
|
|
1213
|
-
const calls = fs.readFileSync(path.join(dir, 'calls.txt'), 'utf8').trim().split('\n')
|
|
1214
|
-
assert.equal(calls.length, 2)
|
|
1215
|
-
assert.ok(calls.some((l) => l.startsWith('--config playwright.identity.config.ts e2e/checkout.spec.ts')))
|
|
1216
|
-
})
|
|
1217
|
-
|
|
1218
|
-
test('audit names the per-app workflow for a monorepo app', () => {
|
|
1219
|
-
const dir = tmpRepo({
|
|
1220
|
-
'apps/web/smoke.manifest.json': JSON.stringify({ profiles: {}, paths: [] }),
|
|
1221
|
-
'.github/workflows/ci.yml': 'on: [pull_request]\njobs:\n s:\n steps:\n - run: node apps/web/scripts/smoke.mjs verdict\n',
|
|
1222
|
-
})
|
|
1223
|
-
assert.match(audit(dir).findings.find((f) => f.kind === 'workflow').note, /golden-paths-apps-web-[0-9a-f]{6}\.yml/)
|
|
1224
|
-
})
|
|
1225
|
-
|
|
1226
|
-
// ---- regressions from the re-verification ----
|
|
1227
|
-
|
|
1228
|
-
test('a failure in a spec we did not run cannot explain a failing exit', async () => {
|
|
1229
|
-
const dir = tmpRepo({ ...SPECS, 'fake-pw.cjs': fakePlaywright(`
|
|
1230
|
-
const ok = { status: 'expected', results: [{ status: 'passed' }] }
|
|
1231
|
-
const bad = { status: 'unexpected', results: [{ status: 'failed' }] }
|
|
1232
|
-
fs.writeFileSync(process.env.PLAYWRIGHT_JSON_OUTPUT_NAME, JSON.stringify({ config: { rootDir: path.resolve('e2e') }, suites: [
|
|
1233
|
-
{ file: 'sign-in.spec.ts', specs: [{ tests: [ok] }] }, { file: 'other.spec.ts', specs: [{ tests: [bad] }] },
|
|
1234
|
-
] }))
|
|
1235
|
-
process.exitCode = 2`) })
|
|
1236
|
-
const c = contract()
|
|
1237
|
-
c.setup.browser = { command: 'node fake-pw.cjs' }
|
|
1238
|
-
c.paths = c.paths.filter((p) => p.id === 'sign-in')
|
|
1239
|
-
const [r] = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: dir })
|
|
1240
|
-
assert.equal(r.status, 'cannot-run')
|
|
1241
|
-
})
|
|
1242
|
-
|
|
1243
|
-
test('the ci command and renderWorkflow refuse an invalid contract', async () => {
|
|
1244
|
-
const c = contract()
|
|
1245
|
-
c.setup.baseUrl = 'http://localhost/$(printf X)/'
|
|
1246
|
-
assert.equal(renderWorkflow(c), null)
|
|
1247
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(c) })
|
|
1248
|
-
assert.equal(await runCli(['ci', '--write', '--contract', path.join(dir, CONTRACT_FILE)], quiet()), 1)
|
|
1249
|
-
assert.equal(fs.existsSync(path.join(dir, WORKFLOW_FILE)), false)
|
|
1250
|
-
})
|
|
1251
|
-
|
|
1252
|
-
test('a deployed verdict refuses unbound results', () => {
|
|
1253
|
-
const pass = [{ id: 'health', status: 'pass' }]
|
|
1254
|
-
assert.match(openResults(pass, { profile: 'prod-preview', deployed: true }).error, /needs results from `run`/)
|
|
1255
|
-
assert.match(openResults({ profile: 'prod-preview', results: pass }, { profile: 'prod-preview', deployed: true }).error, /needs results from `run`/)
|
|
1256
|
-
assert.equal(openResults(pass, { profile: 'pr-local' }).results.length, 1)
|
|
1257
|
-
})
|
|
1258
|
-
|
|
1259
|
-
test('the database url env cannot reuse an engine-owned name, and bad secrets do not crash validation', () => {
|
|
1260
|
-
const c = contract()
|
|
1261
|
-
c.setup.database = { engine: 'postgres', urlEnv: 'BASE_URL' }
|
|
1262
|
-
assert.match(validateContract(c).errors.join('\n'), /urlEnv: BASE_URL is set by the generated workflow/)
|
|
1263
|
-
const d = contract()
|
|
1264
|
-
d.setup.secrets = { generate: { BAD: 'value' } }
|
|
1265
|
-
assert.match(validateContract(d).errors.join('\n'), /secrets.generate: must be an array/)
|
|
1266
|
-
})
|
|
1267
|
-
|
|
1268
|
-
test('the classifier reads commands, not job names, and knows smoke npm scripts', () => {
|
|
1269
|
-
assert.equal(classifyWorkflow('.github/workflows/a.yml', 'on: [pull_request]\njobs:\n s:\n name: smoke.mjs verdict\n steps:\n - run: echo ok\n'), null)
|
|
1270
|
-
assert.deepEqual(classifyWorkflow('.github/workflows/b.yml', 'on: [pull_request]\njobs:\n s:\n steps:\n - run: pnpm test:e2e:smoke\n'), { file: '.github/workflows/b.yml', lane: 'pr', gate: true })
|
|
1271
|
-
assert.equal(classifyWorkflow('.github/workflows/c.yml', 'on: [pull_request]\njobs:\n s:\n steps:\n - run: pnpm smoke:validate\n').gate, false)
|
|
1272
|
-
})
|
|
1273
|
-
|
|
1274
|
-
test('a validate-only PR job does not count as running pr-local', () => {
|
|
1275
|
-
const dir = tmpRepo({
|
|
1276
|
-
'smoke.manifest.json': JSON.stringify({ profiles: { 'pr-local': STANDARD_PROFILES['pr-local'] }, paths: [] }),
|
|
1277
|
-
'.github/workflows/ci.yml': 'on: [pull_request]\njobs:\n v:\n steps:\n - run: node scripts/smoke.mjs validate\n',
|
|
1278
|
-
})
|
|
1279
|
-
assert.ok(audit(dir).findings.some((f) => f.kind === 'drift'))
|
|
1280
|
-
})
|
|
1281
|
-
|
|
1282
|
-
test('a hand-written workflow is never reported as stale generated output', () => {
|
|
1283
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract({ ci: { enabled: false } })), ...SPECS, [WORKFLOW_FILE]: 'name: mine\n' })
|
|
1284
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
1285
|
-
assert.deepEqual(checkDrift(contract({ ci: { enabled: false } }), path.join(dir, CONTRACT_FILE)), [])
|
|
1286
|
-
})
|
|
1287
|
-
|
|
1288
|
-
test('profiles are bound to safe targets and can forbid credentials', () => {
|
|
1289
|
-
const local = { name: 'pr-local', environment: 'local' }
|
|
1290
|
-
const prod = { name: 'prod-preview', environment: 'prod', forbidEnv: ['E2E_TEST_SECRET'] }
|
|
1291
|
-
assert.equal(targetProblem(local, 'http://127.0.0.1:3000'), null)
|
|
1292
|
-
assert.match(targetProblem(local, 'https://prod.example'), /not a loopback address/)
|
|
1293
|
-
assert.match(targetProblem(prod, 'http://prod.example'), /use https/)
|
|
1294
|
-
assert.equal(targetProblem(prod, 'https://prod.example', {}), null)
|
|
1295
|
-
assert.match(targetProblem(prod, 'https://prod.example', { E2E_TEST_SECRET: 'x' }), /forbids E2E_TEST_SECRET/)
|
|
1296
|
-
assert.match(validateProfile('p', { ...STANDARD_PROFILES['prod-preview'], forbidEnv: ['bad name'] }).join('\n'), /forbidEnv/)
|
|
1297
|
-
})
|
|
1298
|
-
|
|
1299
|
-
test('run refuses a local profile against a remote target', async () => {
|
|
1300
|
-
const results = await runPaths(contract(), { profile: 'pr-local', target: 'https://prod.example', appDir: os.tmpdir() })
|
|
1301
|
-
assert.ok(results.every((r) => r.status === 'cannot-run' && /loopback/.test(r.note)))
|
|
1302
|
-
})
|
|
1303
|
-
|
|
1304
|
-
test('a writing request marked as not writing cannot run outside local profiles', () => {
|
|
1305
|
-
const c = contract()
|
|
1306
|
-
c.paths.push({ id: 'order', title: 'order', surface: 'api', request: { method: 'POST', path: '/api/orders' }, observable: '201', mutates: false, profiles: ['pr-local', 'dev-active'] })
|
|
1307
|
-
assert.match(validateContract(c).errors.join('\n'), /POST path marked as not writing runs in dev-active/)
|
|
1308
|
-
c.paths.at(-1).profiles = ['pr-local']
|
|
1309
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
1310
|
-
})
|
|
1311
|
-
|
|
1312
|
-
test('run --boot runs setup.seed before booting and stops on a failed seed', async () => {
|
|
1313
|
-
const port = await freePort()
|
|
1314
|
-
const seeded = bootContract(port, 'node server.cjs')
|
|
1315
|
-
seeded.setup.seed = 'node -e "require(\'fs\').writeFileSync(\'seeded.txt\', \'yes\')"'
|
|
1316
|
-
const dir = tmpRepo({ 'server.cjs': SERVER, [CONTRACT_FILE]: JSON.stringify(seeded) })
|
|
1317
|
-
assert.equal(await runCli(['run', '--contract', path.join(dir, CONTRACT_FILE), '--profile', 'pr-local', '--boot', '--timeout', '20'], quiet()), 0)
|
|
1318
|
-
assert.equal(fs.readFileSync(path.join(dir, 'seeded.txt'), 'utf8'), 'yes')
|
|
1319
|
-
const broken = bootContract(port, 'node server.cjs')
|
|
1320
|
-
broken.setup.seed = 'exit 4'
|
|
1321
|
-
const dir2 = tmpRepo({ 'server.cjs': SERVER, [CONTRACT_FILE]: JSON.stringify(broken) })
|
|
1322
|
-
const q = quiet()
|
|
1323
|
-
assert.equal(await runCli(['run', '--contract', path.join(dir2, CONTRACT_FILE), '--profile', 'pr-local', '--boot'], q), 1)
|
|
1324
|
-
assert.match(q.out.join('\n'), /setup.seed failed/)
|
|
1325
|
-
})
|
|
1326
|
-
|
|
1327
|
-
test('init preview says would, and the verdict prints the report shape', async () => {
|
|
1328
|
-
const dir = tmpRepo({ 'package.json': '{}' })
|
|
1329
|
-
const q = quiet()
|
|
1330
|
-
const cwd = process.cwd()
|
|
1331
|
-
process.chdir(dir)
|
|
1332
|
-
try { assert.equal(await runCli(['init'], q), 0) } finally { process.chdir(cwd) }
|
|
1333
|
-
assert.match(q.out.join('\n'), /would create\s+golden-paths\.json/)
|
|
1334
|
-
assert.match(q.out.join('\n'), /check setup\.boot and setup\.baseUrl/)
|
|
1335
|
-
const d2 = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
1336
|
-
const results = path.join(d2, 'r.json')
|
|
1337
|
-
fs.writeFileSync(results, JSON.stringify([{ id: 'health', status: 'pass', evidence: 'HTTP 200' }, { id: 'sign-in', status: 'fail', note: 'journey failed' }, { id: 'checkout', status: 'pass' }]))
|
|
1338
|
-
const v = quiet()
|
|
1339
|
-
await runCli(['verdict', '--contract', path.join(d2, CONTRACT_FILE), '--profile', 'pr-local', '--results', results], v)
|
|
1340
|
-
assert.match(v.out[0], /^Golden paths: DEAD {2}\(profile pr-local, target http:\/\/127\.0\.0\.1:3000\)$/)
|
|
1341
|
-
assert.ok(v.out.includes('- fail sign-in: journey failed'))
|
|
1342
|
-
})
|
|
1343
|
-
|
|
1344
|
-
test('--paths runs one shard, and the verdict holds to that shard', async () => {
|
|
1345
|
-
const sel = selectPathsForProfile(contract(), 'pr-local', { ids: ['health'] })
|
|
1346
|
-
assert.deepEqual(sel.run.map((p) => p.id), ['health'])
|
|
1347
|
-
assert.throws(() => selectPathsForProfile(contract(), 'pr-local', { ids: ['nope'] }), /unknown path nope/)
|
|
1348
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
1349
|
-
const results = path.join(dir, 'r.json')
|
|
1350
|
-
fs.writeFileSync(results, JSON.stringify(resultsEnvelope([{ id: 'health', status: 'pass' }], { profile: 'pr-local', target: 'http://127.0.0.1:3000', ids: ['health'] })))
|
|
1351
|
-
const out = path.join(dir, 'v.json')
|
|
1352
|
-
assert.equal(await runCli(['verdict', '--contract', path.join(dir, CONTRACT_FILE), '--profile', 'pr-local', '--results', results, '--out', out], quiet()), 0)
|
|
1353
|
-
assert.deepEqual(JSON.parse(fs.readFileSync(out, 'utf8')).paths, ['health'])
|
|
1354
|
-
assert.equal(await runCli(['verdict', '--contract', path.join(dir, CONTRACT_FILE), '--profile', 'pr-local', '--results', results, '--paths', 'sign-in'], quiet()), 1)
|
|
1355
|
-
})
|
|
1356
|
-
|
|
1357
|
-
test('underscore keys park content that validation never reads', () => {
|
|
1358
|
-
const c = contract()
|
|
1359
|
-
c._pendingPaths = [{ id: 'later', title: 'TODO: decide' }]
|
|
1360
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
1361
|
-
})
|
|
1362
|
-
|
|
1363
|
-
test('a cli command with a <placeholder> fails validation', () => {
|
|
1364
|
-
const c = contract({ paths: [{ id: 'w', title: 'w', surface: 'cli', command: 'deploy-check --env <dev|prod>', observable: 'ok', profiles: ['pr-local'] }] })
|
|
1365
|
-
assert.match(validateContract(c).errors.join('\n'), /<placeholder>/)
|
|
1366
|
-
})
|
|
1367
|
-
|
|
1368
|
-
test('audit tells a ci.enabled false repo to keep its own job', () => {
|
|
1369
|
-
const dir = tmpRepo({
|
|
1370
|
-
[CONTRACT_FILE]: JSON.stringify(contract({ ci: { enabled: false } })),
|
|
1371
|
-
'.github/workflows/smoke.yml': 'on:\n push:\n branches: ["mergify/merge-queue/**"]\njobs:\n s:\n steps:\n - run: node scripts/smoke.mjs verdict\n',
|
|
1372
|
-
})
|
|
1373
|
-
assert.match(audit(dir).findings.find((f) => f.kind === 'workflow').note, /keep this job/)
|
|
1374
|
-
})
|
|
1375
|
-
|
|
1376
|
-
// ---- round 3: review and eval regressions ----
|
|
1377
|
-
|
|
1378
|
-
test('forbidEnv cannot be bypassed through setup.env', async () => {
|
|
1379
|
-
const c = contract({ paths: [{ id: 'probe', title: 'probe', surface: 'cli', command: 'echo ok', observable: 'ok', profiles: ['pr-local'] }] })
|
|
1380
|
-
c.profiles = { ...c.profiles, 'pr-local': { ...c.profiles['pr-local'], forbidEnv: ['E2E_TEST_SECRET'] } }
|
|
1381
|
-
c.setup.env = { E2E_TEST_SECRET: 'bypass' }
|
|
1382
|
-
assert.match(validateContract(c).errors.join('\n'), /forbidEnv E2E_TEST_SECRET is set in setup.env/)
|
|
1383
|
-
const [r] = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: os.tmpdir(), env: {} })
|
|
1384
|
-
assert.equal(r.status, 'cannot-run')
|
|
1385
|
-
})
|
|
1386
|
-
|
|
1387
|
-
test('setup.env reaches local runs only', async () => {
|
|
1388
|
-
const c = contract({ paths: [{ id: 'env', title: 'env', surface: 'cli', command: 'printf "$LOCAL_ONLY"', observable: 'x', expect: { stdoutIncludes: 'local' }, profiles: ['pr-local', 'dev-active'] }] })
|
|
1389
|
-
c.setup.env = { LOCAL_ONLY: 'local' }
|
|
1390
|
-
const [local] = await runPaths(c, { profile: 'pr-local', target: 'http://127.0.0.1:9', appDir: os.tmpdir(), env: {} })
|
|
1391
|
-
assert.equal(local.status, 'pass')
|
|
1392
|
-
const [dev] = await runPaths(c, { profile: 'dev-active', target: 'https://dev.example', appDir: os.tmpdir(), env: {} })
|
|
1393
|
-
assert.equal(dev.status, 'fail')
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1396
|
-
test('run --boot refuses an occupied target and notices a boot that died', async () => {
|
|
1397
|
-
const busy = http.createServer((q, s) => { s.writeHead(200); s.end('ok') })
|
|
1398
|
-
await new Promise((r) => busy.listen(0, '127.0.0.1', r))
|
|
1399
|
-
const port = busy.address().port
|
|
1400
|
-
try {
|
|
1401
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(bootContract(port, 'exit 3')) })
|
|
1402
|
-
const q = quiet()
|
|
1403
|
-
assert.equal(await runCli(['run', '--contract', path.join(dir, CONTRACT_FILE), '--profile', 'pr-local', '--boot'], q), 1)
|
|
1404
|
-
assert.match(q.out.join('\n'), /already answers/)
|
|
1405
|
-
} finally { busy.close() }
|
|
1406
|
-
const free = await freePort()
|
|
1407
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(bootContract(free, 'exit 3')) })
|
|
1408
|
-
const q = quiet()
|
|
1409
|
-
const t0 = Date.now()
|
|
1410
|
-
await runCli(['run', '--contract', path.join(dir, CONTRACT_FILE), '--profile', 'pr-local', '--boot', '--timeout', '30'], q)
|
|
1411
|
-
assert.match(q.out.join('\n'), /boot command ended \(exit 3\)/)
|
|
1412
|
-
assert.ok(Date.now() - t0 < 10_000, 'a dead boot is noticed at once, not after the timeout')
|
|
1413
|
-
})
|
|
1414
|
-
|
|
1415
|
-
test('stopApp kills a server that ignores SIGTERM', async () => {
|
|
1416
|
-
const port = await freePort()
|
|
1417
|
-
const stubborn = `process.on('SIGTERM', () => {}); require('node:http').createServer((q, s) => s.end('ok')).listen(${port}, '127.0.0.1')`
|
|
1418
|
-
const dir = tmpRepo({ 'stubborn.cjs': stubborn })
|
|
1419
|
-
const child = bootApp({ setup: { boot: 'node stubborn.cjs' } }, dir)
|
|
1420
|
-
assert.equal(await waitFor(`http://127.0.0.1:${port}/`, 10), null)
|
|
1421
|
-
await stopApp(child, 300)
|
|
1422
|
-
assert.equal(await waitFor(`http://127.0.0.1:${port}/`, 1) !== null, true)
|
|
1423
|
-
})
|
|
1424
|
-
|
|
1425
|
-
test('app directories with YAML-special characters still render valid YAML', () => {
|
|
1426
|
-
const wf = renderWorkflow(contract(), { appRel: 'apps/web: preview' })
|
|
1427
|
-
assert.match(wf, /name: "Golden paths \(apps\/web: preview\)"/)
|
|
1428
|
-
assert.match(wf, /working-directory: "apps\/web: preview"/)
|
|
1429
|
-
})
|
|
1430
|
-
|
|
1431
|
-
test('audit flags an engine copy whose content differs at the same version', () => {
|
|
1432
|
-
const dir = tmpRepo({ [CONTRACT_FILE]: JSON.stringify(contract()), ...SPECS })
|
|
1433
|
-
applyPlan(initPlan(path.join(dir, CONTRACT_FILE)).plan, { write: true })
|
|
1434
|
-
assert.deepEqual(audit(dir).findings.filter((f) => f.kind === 'engine'), [])
|
|
1435
|
-
const core = path.join(dir, ENGINE_FILE.replace('golden-path.mjs', 'golden-path-core.mjs'))
|
|
1436
|
-
fs.appendFileSync(core, '\n// edited\n')
|
|
1437
|
-
assert.match(audit(dir).findings.find((f) => f.kind === 'engine').note, /differs from this skill's/)
|
|
1438
|
-
})
|
|
1439
|
-
|
|
1440
|
-
test('echoed command text is not a gate', () => {
|
|
1441
|
-
assert.equal(classifyWorkflow('.github/workflows/ci.yml', 'on: [pull_request]\njobs:\n s:\n steps:\n - run: echo "node smoke.mjs run"\n'), null)
|
|
1442
|
-
})
|
|
1443
|
-
|
|
1444
|
-
test('a refused-write probe can run in production with a stated reason', () => {
|
|
1445
|
-
const c = contract()
|
|
1446
|
-
c.paths.push({ id: 'dcr-closed', title: 'registration is closed', surface: 'api', request: { method: 'POST', path: '/oauth/register' }, observable: '404', expect: { status: 404 }, mutates: false, profiles: ['prod-preview'] })
|
|
1447
|
-
assert.match(validateContract(c).errors.join('\n'), /explain in "nonWriting"/)
|
|
1448
|
-
c.paths.at(-1).nonWriting = 'the candidate answers 404 before parsing, so nothing is stored'
|
|
1449
|
-
assert.deepEqual(validateContract(c).errors, [])
|
|
1450
|
-
})
|
|
1451
|
-
|
|
1452
|
-
test('expect checks headers and non-empty JSON values', () => {
|
|
1453
|
-
const p = { expect: { headers: { 'Content-Type': 'text/markdown' }, jsonNonEmpty: ['keys'] } }
|
|
1454
|
-
assert.equal(judgeHttp(p, 200, '{"keys":[1]}', { 'content-type': 'text/markdown; charset=utf-8' }).status, 'pass')
|
|
1455
|
-
assert.match(judgeHttp(p, 200, '{"keys":[]}', { 'content-type': 'text/markdown' }).note, /keys is empty/)
|
|
1456
|
-
assert.match(judgeHttp(p, 200, '{"keys":[1]}', { 'content-type': 'text/html' }).note, /header Content-Type/)
|
|
1457
|
-
})
|