@ikon85/agent-workflow-kit 0.43.0 → 0.44.0
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/.agents/skills/grill-me/SKILL.md +3 -1
- package/.agents/skills/grill-with-docs/SKILL.md +3 -1
- package/.agents/skills/kit-update/SKILL.md +9 -11
- package/.agents/skills/orchestrate-wave/SKILL.md +8 -8
- package/.agents/skills/setup-workflow/SKILL.md +20 -18
- package/.agents/skills/setup-workflow/board-sync.md +4 -1
- package/.agents/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.agents/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.agents/skills/to-issues/SKILL.md +43 -0
- package/.agents/skills/wrapup/SKILL.md +98 -54
- package/.claude/skills/grill-me/SKILL.md +3 -1
- package/.claude/skills/grill-me-codex/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs/SKILL.md +3 -1
- package/.claude/skills/grill-with-docs-codex/SKILL.md +3 -1
- package/.claude/skills/kit-update/SKILL.md +9 -11
- package/.claude/skills/orchestrate-wave/SKILL.md +8 -8
- package/.claude/skills/setup-workflow/SKILL.md +20 -18
- package/.claude/skills/setup-workflow/board-sync.md +4 -1
- package/.claude/skills/setup-workflow/workflow-advisories.md +3 -2
- package/.claude/skills/setup-workflow/worktree-lifecycle.md +36 -31
- package/.claude/skills/to-issues/SKILL.md +43 -0
- package/.claude/skills/wrapup/SKILL.md +98 -54
- package/README.md +249 -7
- package/agent-workflow-kit.package.json +42 -42
- package/package.json +16 -12
- package/scripts/marker_lib.py +3 -0
- package/scripts/profile_globs.py +26 -45
- package/scripts/release-delta-guard.mjs +26 -9
- package/scripts/worktree-lifecycle/README.md +57 -160
- package/scripts/worktree-lifecycle/capabilities.json +2 -3
- package/scripts/worktree-lifecycle/classify.py +480 -0
- package/scripts/worktree-lifecycle/cleanup.py +9 -29
- package/scripts/worktree-lifecycle/core.py +68 -1429
- package/scripts/worktree-lifecycle/ignore_seed.py +1 -1
- package/scripts/worktree-lifecycle/profile.py +46 -29
- package/scripts/worktree-lifecycle/setup.py +7 -19
- package/scripts/wrapup-land.py +1292 -545
- package/src/consumer-migrations.json +1 -16
- package/src/lib/bundle.mjs +31 -1
- package/docs/adr/0001-consumer-divergence-policy.md +0 -53
- package/docs/adr/0002-capability-gated-orchestration.md +0 -70
- package/docs/adr/0003-kit-core-and-project-extension-lifecycle.md +0 -63
- package/docs/adr/0004-release-intent-is-a-version-tag.md +0 -64
- package/docs/adr/0005-to-issues-is-the-planning-facade.md +0 -42
- package/docs/adr/0006-routing-knowledge-access-and-policy-are-separate.md +0 -95
- package/docs/adr/0007-session-teardown-requires-provenance-bound-ownership.md +0 -88
- package/docs/adr/0008-planning-ignore-rules-are-offered-never-installed.md +0 -84
- package/docs/adr/0009-teardown-authority-is-stateless-repository-classification.md +0 -101
- package/docs/adr/0010-model-roster-replaces-the-optimization-dial.md +0 -87
- package/docs/agents/board-sync.md +0 -74
- package/docs/agents/code-review.md +0 -14
- package/docs/agents/workflow-capabilities.json +0 -28
- package/docs/research/agent-task-taxonomy-benchmark-coverage.md +0 -451
- package/docs/research/benchlm-routing-source.md +0 -198
- package/docs/research/consumer-owned-protocol-files.md +0 -238
- package/docs/research/frontend-agent-benchmarks.md +0 -282
- package/docs/research/model-effort-routing-benchmarks.md +0 -261
- package/docs/research/provider-neutral-agent-routing.md +0 -207
- package/docs/research/wave-152-consumer-acceptance.md +0 -98
- package/docs/research/wave-43-script-hook-census.md +0 -128
- package/scripts/build-kit.mjs +0 -117
- package/scripts/build-kit.test.mjs +0 -207
- package/scripts/census/census.test.mjs +0 -252
- package/scripts/census/state.test.mjs +0 -15
- package/scripts/census/transaction.test.mjs +0 -103
- package/scripts/census-contract.test.mjs +0 -227
- package/scripts/check-kit-staleness.mjs +0 -45
- package/scripts/check-kit-staleness.test.mjs +0 -19
- package/scripts/codex-exec-scenarios/fake-codex.mjs +0 -165
- package/scripts/codex-exec.test.mjs +0 -871
- package/scripts/grill-census-wiring-guard.mjs +0 -87
- package/scripts/grill-census-wiring-guard.test.mjs +0 -107
- package/scripts/kit-release.test.mjs +0 -273
- package/scripts/kit-update-pr.test.mjs +0 -151
- package/scripts/lib/audit-refs.mjs +0 -146
- package/scripts/lib/scrub.mjs +0 -113
- package/scripts/lib/scrub.test.mjs +0 -187
- package/scripts/memory-lifecycle/memory-lifecycle.test.mjs +0 -170
- package/scripts/portability_profile_scan.py +0 -53
- package/scripts/release-delta-guard.test.mjs +0 -174
- package/scripts/release-parity.test.mjs +0 -90
- package/scripts/release-state.test.mjs +0 -360
- package/scripts/test_anchor_table.py +0 -69
- package/scripts/test_board_bootstrap.py +0 -348
- package/scripts/test_board_sync.py +0 -208
- package/scripts/test_board_sync_create_idempotency.py +0 -117
- package/scripts/test_board_sync_wave_title.py +0 -120
- package/scripts/test_census_backstop.py +0 -712
- package/scripts/test_census_forward_contract.py +0 -91
- package/scripts/test_census_update_contract.test.mjs +0 -454
- package/scripts/test_codex_adapter_sync_contract.py +0 -169
- package/scripts/test_dist_kit_smoke.py +0 -220
- package/scripts/test_drift_guard_diagnostics.py +0 -295
- package/scripts/test_issue_claim_contract.py +0 -169
- package/scripts/test_kit_docs_language_census.py +0 -178
- package/scripts/test_marker_lib.py +0 -162
- package/scripts/test_orchestrate_wave_contract.py +0 -381
- package/scripts/test_pr_body_check.py +0 -264
- package/scripts/test_profile_globs.py +0 -280
- package/scripts/test_program_planning_contract.py +0 -133
- package/scripts/test_release_authorization_contract.py +0 -101
- package/scripts/test_render_anchor.py +0 -267
- package/scripts/test_retro_wrapup_contract.py +0 -153
- package/scripts/test_skill_code_review_seed.py +0 -89
- package/scripts/test_skill_codex_exec_lifecycle.py +0 -123
- package/scripts/test_skill_frontmatter_lint.py +0 -234
- package/scripts/test_skill_gh_lint.py +0 -124
- package/scripts/test_skill_language_census.py +0 -254
- package/scripts/test_skill_optional_readiness.py +0 -171
- package/scripts/test_skill_portability_lint.py +0 -866
- package/scripts/test_skill_precommit_template.py +0 -30
- package/scripts/test_skill_publish_audit.py +0 -358
- package/scripts/test_skill_readiness_contract.py +0 -264
- package/scripts/test_skill_readiness_preflight.py +0 -180
- package/scripts/test_skill_required_readiness.py +0 -233
- package/scripts/test_skill_selfcontainment_lint.py +0 -255
- package/scripts/test_skill_setup_workflow_seeds.py +0 -691
- package/scripts/test_skill_stale_name_lint.py +0 -126
- package/scripts/test_skill_surface_refs.py +0 -300
- package/scripts/test_skill_trailing_artifact_lint.py +0 -71
- package/scripts/test_tdd_contract.py +0 -78
- package/scripts/test_worktree_ignore_seed.py +0 -320
- package/scripts/test_worktree_setup_base_guard.py +0 -140
- package/scripts/test_worktree_wrapup_contract.py +0 -1737
- package/scripts/test_wrapup_land.py +0 -428
- package/scripts/worktree-lifecycle/session.py +0 -1857
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""portability_profile_scan.py — profile-VALUE literal scan (#1878, Welle 49
|
|
3
|
-
Slice 7, extends the portability lint in test_skill_portability_lint.py).
|
|
4
|
-
|
|
5
|
-
A published (generic/vendored) skill body must reference the board profile
|
|
6
|
-
(`docs/agents/board-sync.md`, loaded by `board_config.py`) by KEY —
|
|
7
|
-
`prMarkers.retroValues`, `headings.vorBau` — never MANDATE the literal VALUE
|
|
8
|
-
a profile happens to configure (e.g. testreporter's `gefahren`/`übersprungen`,
|
|
9
|
-
`Vor Bau zu klären`). A consumer whose profile configures different words
|
|
10
|
-
silently fails a skill that hardcoded testreporter's wording as required —
|
|
11
|
-
the same drift class `test_skill_portability_lint.py`'s CONSTANT_PATTERNS
|
|
12
|
-
scan already closes for opaque board node/option ids; this closes it for the
|
|
13
|
-
human-facing convention words instead.
|
|
14
|
-
|
|
15
|
-
A line that also names the profile key is a reference, not a hardcode —
|
|
16
|
-
exempt (mirrors CONSTANT_PATTERNS' own exempt rule). A line carrying
|
|
17
|
-
`portability-lint: ok` is exempt too (deliberate doc example, e.g. a
|
|
18
|
-
copy-paste-ready marker/heading example where inlining the key would corrupt
|
|
19
|
-
the example).
|
|
20
|
-
"""
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import re
|
|
24
|
-
|
|
25
|
-
EXEMPT = "portability-lint: ok"
|
|
26
|
-
|
|
27
|
-
# (label, literal pattern, profile-key substring whose presence on the same
|
|
28
|
-
# line exempts the match — a reference, not a hardcode).
|
|
29
|
-
PROFILE_VALUE_PATTERNS = [
|
|
30
|
-
# prMarkers.retroValues — the two configured PR "**Retro:**"-line words.
|
|
31
|
-
# Backtick-quoted only: bare "gefahren"/"übersprungen" are ordinary German
|
|
32
|
-
# words (past participles of fahren/überspringen) outside this
|
|
33
|
-
# convention — unquoted would false-positive on normal prose (e.g. "Retro
|
|
34
|
-
# schon gefahren?").
|
|
35
|
-
("prMarkers.retroValues literal", re.compile(r"`(?:gefahren|übersprungen)`"), "retroValues"),
|
|
36
|
-
# headings.vorBau — the configured HITL heading text. The phrase is
|
|
37
|
-
# specific enough that no quoting requirement is needed.
|
|
38
|
-
("headings.vorBau literal", re.compile(r"Vor Bau zu klären"), "vorBau"),
|
|
39
|
-
]
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def find_profile_value_literals(text: str) -> list[tuple[int, str, str]]:
|
|
43
|
-
"""Return (line_no, label, line) for every hardcoded MANDATORY profile-value
|
|
44
|
-
literal, minus lines that also reference the profile key (a profile-driven
|
|
45
|
-
reference, not a hardcode) or carry the exempt marker."""
|
|
46
|
-
out = []
|
|
47
|
-
for n, line in enumerate(text.splitlines(), 1):
|
|
48
|
-
if EXEMPT in line:
|
|
49
|
-
continue
|
|
50
|
-
for label, pattern, key in PROFILE_VALUE_PATTERNS:
|
|
51
|
-
if pattern.search(line) and key not in line:
|
|
52
|
-
out.append((n, label, line.strip()))
|
|
53
|
-
return out
|
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
-
import { tmpdir } from 'node:os';
|
|
5
|
-
import { dirname, join } from 'node:path';
|
|
6
|
-
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { buildKit } from './build-kit.mjs';
|
|
8
|
-
import {
|
|
9
|
-
assessRelease, manifestDelta, packedPayloadManifest,
|
|
10
|
-
} from './release-delta-guard.mjs';
|
|
11
|
-
|
|
12
|
-
const file = (path, sha256) => ({ path, sha256 });
|
|
13
|
-
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
14
|
-
|
|
15
|
-
test('fresh build manifest hashes match the actual npm package payload', async () => {
|
|
16
|
-
const tempRoot = await mkdtemp(join(tmpdir(), 'awkit-packed-manifest-test-'));
|
|
17
|
-
try {
|
|
18
|
-
const distDir = join(tempRoot, 'dist');
|
|
19
|
-
await buildKit({ repoRoot, distDir });
|
|
20
|
-
const built = JSON.parse(await readFile(join(distDir, 'agent-workflow-kit.package.json'), 'utf8'));
|
|
21
|
-
const packed = await packedPayloadManifest({ repoRoot, manifest: built });
|
|
22
|
-
assert.deepEqual(manifestDelta(built, packed), { added: [], removed: [], changed: [] });
|
|
23
|
-
} finally {
|
|
24
|
-
await rm(tempRoot, { recursive: true, force: true });
|
|
25
|
-
}
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
test('an unbumped shipped change is blocked with its concrete delta', () => {
|
|
29
|
-
const result = assessRelease({
|
|
30
|
-
baseVersion: '1.2.3', currentVersion: '1.2.3',
|
|
31
|
-
baseManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
|
|
32
|
-
builtManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'new')] },
|
|
33
|
-
checkedManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
|
|
34
|
-
payloadManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'new')] },
|
|
35
|
-
});
|
|
36
|
-
assert.equal(result.ok, false);
|
|
37
|
-
assert.deepEqual(result.delta.changed, ['skill.md']);
|
|
38
|
-
assert.match(result.errors.join('\n'), /skill\.md/);
|
|
39
|
-
assert.match(result.errors.join('\n'), /version remains 1\.2\.3/);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('an npm runtime change outside the Consumer manifest still requires a release', () => {
|
|
43
|
-
const consumer = { kitVersion: '1.2.3', files: [file('skill.md', 'same')] };
|
|
44
|
-
const result = assessRelease({
|
|
45
|
-
baseVersion: '1.2.3',
|
|
46
|
-
currentVersion: '1.2.3',
|
|
47
|
-
baseManifest: consumer,
|
|
48
|
-
builtManifest: consumer,
|
|
49
|
-
checkedManifest: consumer,
|
|
50
|
-
payloadManifest: consumer,
|
|
51
|
-
basePackagePayload: { files: [file('src/cli.mjs', 'old')] },
|
|
52
|
-
currentPackagePayload: { files: [file('src/cli.mjs', 'new')] },
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
assert.equal(result.ok, false);
|
|
56
|
-
assert.deepEqual(result.delta.changed, ['src/cli.mjs']);
|
|
57
|
-
assert.equal(result.recommendedBump, 'patch');
|
|
58
|
-
assert.match(result.errors.join('\n'), /version remains 1\.2\.3/);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('removing accidentally packed seed-only project stubs is a minor change', () => {
|
|
62
|
-
const consumer = { kitVersion: '1.3.0', files: [file('skill.md', 'same')] };
|
|
63
|
-
const result = assessRelease({
|
|
64
|
-
baseVersion: '1.2.3',
|
|
65
|
-
currentVersion: '1.3.0',
|
|
66
|
-
baseManifest: { ...consumer, kitVersion: '1.2.3' },
|
|
67
|
-
builtManifest: consumer,
|
|
68
|
-
checkedManifest: consumer,
|
|
69
|
-
payloadManifest: consumer,
|
|
70
|
-
basePackagePayload: {
|
|
71
|
-
files: [
|
|
72
|
-
file('src/cli.mjs', 'same'),
|
|
73
|
-
file('docs/agents/issue-tracker.md', 'filled-project-layer'),
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
currentPackagePayload: { files: [file('src/cli.mjs', 'same')] },
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
assert.equal(result.ok, true);
|
|
80
|
-
assert.deepEqual(result.delta.removed, ['docs/agents/issue-tracker.md']);
|
|
81
|
-
assert.equal(result.recommendedBump, 'minor');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('dead checked-manifest entries are rejected', () => {
|
|
85
|
-
const result = assessRelease({
|
|
86
|
-
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
87
|
-
baseManifest: { kitVersion: '1.2.3', files: [] },
|
|
88
|
-
builtManifest: { kitVersion: '1.3.0', files: [] },
|
|
89
|
-
checkedManifest: { kitVersion: '1.3.0', files: [file('gone.md', 'old')] },
|
|
90
|
-
payloadManifest: { kitVersion: '1.3.0', files: [] },
|
|
91
|
-
});
|
|
92
|
-
assert.equal(result.ok, false);
|
|
93
|
-
assert.match(result.errors.join('\n'), /dead entry: gone\.md/);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('a matching regenerated manifest and semantic bump pass', () => {
|
|
97
|
-
const current = { kitVersion: '1.3.0', files: [file('new.md', 'one')] };
|
|
98
|
-
const result = assessRelease({
|
|
99
|
-
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
100
|
-
baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
|
|
101
|
-
checkedManifest: current, payloadManifest: current,
|
|
102
|
-
});
|
|
103
|
-
assert.equal(result.ok, true);
|
|
104
|
-
assert.equal(result.recommendedBump, 'minor');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('shipped content rejects downgrade and malformed version transitions', () => {
|
|
108
|
-
for (const currentVersion of ['1.1.0', 'banana']) {
|
|
109
|
-
const current = { kitVersion: currentVersion, files: [file('skill.md', 'new')] };
|
|
110
|
-
const result = assessRelease({
|
|
111
|
-
baseVersion: '1.2.3', currentVersion,
|
|
112
|
-
baseManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'old')] },
|
|
113
|
-
builtManifest: current, checkedManifest: current, payloadManifest: current,
|
|
114
|
-
});
|
|
115
|
-
assert.equal(result.ok, false, currentVersion);
|
|
116
|
-
assert.match(result.errors.join('\n'), /invalid version transition: 1\.2\.3 ->/);
|
|
117
|
-
}
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('a bump on top of an untagged previous release is blocked', () => {
|
|
121
|
-
const current = { kitVersion: '1.3.0', files: [file('new.md', 'one')] };
|
|
122
|
-
const result = assessRelease({
|
|
123
|
-
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
124
|
-
baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
|
|
125
|
-
checkedManifest: current, payloadManifest: current,
|
|
126
|
-
baseTag: { name: 'v1.2.3', exists: false, repoHasTags: true },
|
|
127
|
-
});
|
|
128
|
-
assert.equal(result.ok, false);
|
|
129
|
-
assert.match(result.errors.join('\n'), /1\.2\.3 is still awaiting-tag/);
|
|
130
|
-
assert.match(result.errors.join('\n'), /v1\.2\.3/);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('a bump passes once the previous release carries its tag', () => {
|
|
134
|
-
const current = { kitVersion: '1.3.0', files: [file('new.md', 'one')] };
|
|
135
|
-
const result = assessRelease({
|
|
136
|
-
baseVersion: '1.2.3', currentVersion: '1.3.0',
|
|
137
|
-
baseManifest: { kitVersion: '1.2.3', files: [] }, builtManifest: current,
|
|
138
|
-
checkedManifest: current, payloadManifest: current,
|
|
139
|
-
baseTag: { name: 'v1.2.3', exists: true, repoHasTags: true },
|
|
140
|
-
});
|
|
141
|
-
assert.equal(result.ok, true);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
test('a never-tagged repository is not blocked from its first release', () => {
|
|
145
|
-
const current = { kitVersion: '0.1.0', files: [file('new.md', 'one')] };
|
|
146
|
-
const result = assessRelease({
|
|
147
|
-
baseVersion: '0.0.0', currentVersion: '0.1.0',
|
|
148
|
-
baseManifest: { kitVersion: '0.0.0', files: [] }, builtManifest: current,
|
|
149
|
-
checkedManifest: current, payloadManifest: current,
|
|
150
|
-
baseTag: { name: 'v0.0.0', exists: false, repoHasTags: false },
|
|
151
|
-
});
|
|
152
|
-
assert.equal(result.ok, true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
test('an untagged base blocks nothing while the version stays put', () => {
|
|
156
|
-
const same = { kitVersion: '1.2.3', files: [file('skill.md', 'one')] };
|
|
157
|
-
const result = assessRelease({
|
|
158
|
-
baseVersion: '1.2.3', currentVersion: '1.2.3',
|
|
159
|
-
baseManifest: same, builtManifest: same, checkedManifest: same, payloadManifest: same,
|
|
160
|
-
baseTag: { name: 'v1.2.3', exists: false, repoHasTags: true },
|
|
161
|
-
});
|
|
162
|
-
assert.equal(result.ok, true);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('actual npm payload drift is blocked even when checked and built manifests match', () => {
|
|
166
|
-
const scrubbed = { kitVersion: '1.2.3', files: [file('skill.md', 'scrubbed')] };
|
|
167
|
-
const result = assessRelease({
|
|
168
|
-
baseVersion: '1.2.3', currentVersion: '1.2.3',
|
|
169
|
-
baseManifest: scrubbed, builtManifest: scrubbed, checkedManifest: scrubbed,
|
|
170
|
-
payloadManifest: { kitVersion: '1.2.3', files: [file('skill.md', 'source')] },
|
|
171
|
-
});
|
|
172
|
-
assert.equal(result.ok, false);
|
|
173
|
-
assert.match(result.errors.join('\n'), /npm package payload.*changed: skill\.md/);
|
|
174
|
-
});
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { mkdtemp, mkdir, rm, writeFile } from 'node:fs/promises';
|
|
5
|
-
import { tmpdir } from 'node:os';
|
|
6
|
-
import { join } from 'node:path';
|
|
7
|
-
import { promisify } from 'node:util';
|
|
8
|
-
import { assertReleaseParity, releaseIdentityFromTarball } from './release-parity.mjs';
|
|
9
|
-
|
|
10
|
-
const exec = promisify(execFile);
|
|
11
|
-
|
|
12
|
-
const identity = {
|
|
13
|
-
name: '@ikon85/agent-workflow-kit',
|
|
14
|
-
version: '1.2.3',
|
|
15
|
-
tarballIntegrity: 'sha512-example',
|
|
16
|
-
manifestSha256: 'abc123',
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
test('identical local, npm, and GitHub release identities have parity', () => {
|
|
20
|
-
assert.deepEqual(assertReleaseParity({ local: identity, npm: { ...identity }, github: { ...identity } }), identity);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test('a registry or GitHub content mismatch fails closed', () => {
|
|
24
|
-
assert.throws(() => assertReleaseParity({
|
|
25
|
-
local: identity,
|
|
26
|
-
npm: { ...identity, manifestSha256: 'different' },
|
|
27
|
-
github: { ...identity },
|
|
28
|
-
}), /npm manifestSha256 mismatch/);
|
|
29
|
-
assert.throws(() => assertReleaseParity({
|
|
30
|
-
local: identity,
|
|
31
|
-
npm: { ...identity },
|
|
32
|
-
github: { ...identity, tarballIntegrity: 'different' },
|
|
33
|
-
}), /github tarballIntegrity mismatch/);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test('a package tarball produces a deterministic content identity', async () => {
|
|
37
|
-
const root = await mkdtemp(join(tmpdir(), 'release-parity-'));
|
|
38
|
-
const packageDir = join(root, 'package');
|
|
39
|
-
const tarball = join(root, 'kit.tgz');
|
|
40
|
-
try {
|
|
41
|
-
await mkdir(packageDir);
|
|
42
|
-
await writeFile(join(packageDir, 'package.json'), '{"name":"kit","version":"2.0.0"}\n');
|
|
43
|
-
await writeFile(join(packageDir, 'agent-workflow-kit.package.json'), '{"kitVersion":"2.0.0","files":[]}\n');
|
|
44
|
-
await exec('tar', ['-czf', tarball, '-C', root, 'package']);
|
|
45
|
-
const first = await releaseIdentityFromTarball(tarball);
|
|
46
|
-
const second = await releaseIdentityFromTarball(tarball);
|
|
47
|
-
assert.equal(first.name, 'kit');
|
|
48
|
-
assert.equal(first.version, '2.0.0');
|
|
49
|
-
assert.match(first.tarballIntegrity, /^sha512-/);
|
|
50
|
-
assert.match(first.manifestSha256, /^[a-f0-9]{64}$/);
|
|
51
|
-
assert.deepEqual(first, second);
|
|
52
|
-
} finally { await rm(root, { recursive: true, force: true }); }
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
test('consumer parity proves npm↔github and matches the installed copy without re-packing', async () => {
|
|
56
|
-
const { assertConsumerReleaseParity } = await import('./release-parity.mjs');
|
|
57
|
-
const installed = { name: identity.name, version: identity.version, manifestSha256: identity.manifestSha256 };
|
|
58
|
-
assert.deepEqual(
|
|
59
|
-
assertConsumerReleaseParity({ installed, npm: { ...identity }, github: { ...identity } }),
|
|
60
|
-
identity,
|
|
61
|
-
);
|
|
62
|
-
assert.throws(() => assertConsumerReleaseParity({
|
|
63
|
-
installed, npm: { ...identity }, github: { ...identity, tarballIntegrity: 'different' },
|
|
64
|
-
}), /github tarballIntegrity mismatch/);
|
|
65
|
-
assert.throws(() => assertConsumerReleaseParity({
|
|
66
|
-
installed: { ...installed, manifestSha256: 'different' },
|
|
67
|
-
npm: { ...identity }, github: { ...identity },
|
|
68
|
-
}), /installed manifestSha256 mismatch/);
|
|
69
|
-
assert.throws(() => assertConsumerReleaseParity({
|
|
70
|
-
installed: { ...installed, version: '9.9.9' },
|
|
71
|
-
npm: { ...identity }, github: { ...identity },
|
|
72
|
-
}), /installed version mismatch/);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test('an installed kit directory yields a content identity without npm pack', async () => {
|
|
76
|
-
const { installedIdentityFromDir } = await import('./release-parity.mjs');
|
|
77
|
-
const root = await mkdtemp(join(tmpdir(), 'release-parity-installed-'));
|
|
78
|
-
try {
|
|
79
|
-
const manifest = JSON.stringify({ kitVersion: '1.2.3', files: [] });
|
|
80
|
-
await writeFile(join(root, 'package.json'), JSON.stringify({ name: identity.name, version: '1.2.3' }));
|
|
81
|
-
await writeFile(join(root, 'agent-workflow-kit.package.json'), manifest);
|
|
82
|
-
const installed = await installedIdentityFromDir(root);
|
|
83
|
-
assert.equal(installed.name, identity.name);
|
|
84
|
-
assert.equal(installed.version, '1.2.3');
|
|
85
|
-
assert.equal(typeof installed.manifestSha256, 'string');
|
|
86
|
-
assert.equal('tarballIntegrity' in installed, false);
|
|
87
|
-
} finally {
|
|
88
|
-
await rm(root, { recursive: true, force: true });
|
|
89
|
-
}
|
|
90
|
-
});
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
import { test } from 'node:test';
|
|
2
|
-
import assert from 'node:assert/strict';
|
|
3
|
-
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
-
import { execFileSync } from 'node:child_process';
|
|
5
|
-
import { randomUUID } from 'node:crypto';
|
|
6
|
-
import { dirname, join } from 'node:path';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import {
|
|
10
|
-
createCommandAdapter, DEFAULT_VISIBILITY_DELAYS_MS, githubReleaseArgs, inspectRelease,
|
|
11
|
-
isMissingRelease, npmTarballFilename, reconcileRelease,
|
|
12
|
-
} from './release-state.mjs';
|
|
13
|
-
|
|
14
|
-
const REPO = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
15
|
-
|
|
16
|
-
const identity = {
|
|
17
|
-
name: '@ikon85/agent-workflow-kit', version: '1.2.3',
|
|
18
|
-
tarballIntegrity: 'sha512-example', manifestSha256: 'abc123',
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
function adapter({
|
|
22
|
-
npmPublished = false,
|
|
23
|
-
githubReleased = false,
|
|
24
|
-
npmIdentity = identity,
|
|
25
|
-
npmInvisibleReads = 0,
|
|
26
|
-
githubInvisibleReads = 0,
|
|
27
|
-
publishNpmError,
|
|
28
|
-
createGithubError,
|
|
29
|
-
npmReadErrorAfterPublish,
|
|
30
|
-
} = {}) {
|
|
31
|
-
const events = [];
|
|
32
|
-
let npmExists = npmPublished;
|
|
33
|
-
let githubExists = githubReleased;
|
|
34
|
-
let npmReadsUntilVisible = npmInvisibleReads;
|
|
35
|
-
let githubReadsUntilVisible = githubInvisibleReads;
|
|
36
|
-
return {
|
|
37
|
-
events,
|
|
38
|
-
local: async () => ({ identity, tarball: '/tmp/kit.tgz' }),
|
|
39
|
-
npm: async () => {
|
|
40
|
-
events.push('read npm');
|
|
41
|
-
if (!npmExists) return null;
|
|
42
|
-
if (npmReadErrorAfterPublish) throw npmReadErrorAfterPublish;
|
|
43
|
-
if (npmReadsUntilVisible > 0) {
|
|
44
|
-
npmReadsUntilVisible -= 1;
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
return npmIdentity;
|
|
48
|
-
},
|
|
49
|
-
github: async () => {
|
|
50
|
-
events.push('read github');
|
|
51
|
-
if (!githubExists) return null;
|
|
52
|
-
if (githubReadsUntilVisible > 0) {
|
|
53
|
-
githubReadsUntilVisible -= 1;
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
return identity;
|
|
57
|
-
},
|
|
58
|
-
publishNpm: async () => {
|
|
59
|
-
events.push('publish npm');
|
|
60
|
-
if (publishNpmError) throw publishNpmError;
|
|
61
|
-
npmExists = true;
|
|
62
|
-
},
|
|
63
|
-
createGithub: async () => {
|
|
64
|
-
events.push('create github');
|
|
65
|
-
if (createGithubError) throw createGithubError;
|
|
66
|
-
githubExists = true;
|
|
67
|
-
},
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function visibilityOptions(attempts, delays = [], progress = []) {
|
|
72
|
-
return {
|
|
73
|
-
visibility: {
|
|
74
|
-
delaysMs: Array.from({ length: attempts - 1 }, (_, index) => 10 * (2 ** index)),
|
|
75
|
-
sleep: async (delay) => delays.push(delay),
|
|
76
|
-
log: (message) => progress.push(message),
|
|
77
|
-
},
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
test('the default npm visibility window spans at least five minutes', () => {
|
|
82
|
-
assert.ok(
|
|
83
|
-
DEFAULT_VISIBILITY_DELAYS_MS.reduce((total, delay) => total + delay, 0) >= 300_000,
|
|
84
|
-
);
|
|
85
|
-
assert.deepEqual(DEFAULT_VISIBILITY_DELAYS_MS, [
|
|
86
|
-
1_000, 2_000, 4_000, 8_000, 16_000, 32_000, 64_000, 128_000, 256_000,
|
|
87
|
-
]);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('a fresh release publishes npm, verifies registry readback, then creates GitHub release', async () => {
|
|
91
|
-
const fixture = adapter();
|
|
92
|
-
assert.deepEqual(await reconcileRelease(fixture), { status: 'released', identity });
|
|
93
|
-
assert.deepEqual(fixture.events, [
|
|
94
|
-
'read npm', 'read github', 'publish npm', 'read npm', 'create github', 'read github',
|
|
95
|
-
]);
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
test('npm visibility is retried with bounded backoff after a successful publish', async () => {
|
|
99
|
-
const fixture = adapter({ npmInvisibleReads: 2 });
|
|
100
|
-
const delays = [];
|
|
101
|
-
const progress = [];
|
|
102
|
-
assert.deepEqual(
|
|
103
|
-
await reconcileRelease(fixture, visibilityOptions(4, delays, progress)),
|
|
104
|
-
{ status: 'released', identity },
|
|
105
|
-
);
|
|
106
|
-
assert.deepEqual(fixture.events, [
|
|
107
|
-
'read npm', 'read github', 'publish npm',
|
|
108
|
-
'read npm', 'read npm', 'read npm',
|
|
109
|
-
'create github', 'read github',
|
|
110
|
-
]);
|
|
111
|
-
assert.deepEqual(delays, [10, 20]);
|
|
112
|
-
assert.deepEqual(progress, [
|
|
113
|
-
'npm visibility: attempt 1/4',
|
|
114
|
-
'npm visibility: attempt 2/4',
|
|
115
|
-
'npm visibility: attempt 3/4',
|
|
116
|
-
'GitHub visibility: attempt 1/4',
|
|
117
|
-
]);
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test('GitHub visibility is retried with bounded backoff after a successful create', async () => {
|
|
121
|
-
const fixture = adapter({ npmPublished: true, githubInvisibleReads: 2 });
|
|
122
|
-
const delays = [];
|
|
123
|
-
assert.deepEqual(
|
|
124
|
-
await reconcileRelease(fixture, visibilityOptions(4, delays)),
|
|
125
|
-
{ status: 'released', identity },
|
|
126
|
-
);
|
|
127
|
-
assert.deepEqual(fixture.events, [
|
|
128
|
-
'read npm', 'read github', 'create github', 'read github', 'read github', 'read github',
|
|
129
|
-
]);
|
|
130
|
-
assert.deepEqual(delays, [10, 20]);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('permanent npm absence fails at the bound with post-publish phase evidence', async () => {
|
|
134
|
-
const fixture = adapter({ npmInvisibleReads: 4 });
|
|
135
|
-
const delays = [];
|
|
136
|
-
const progress = [];
|
|
137
|
-
await assert.rejects(
|
|
138
|
-
reconcileRelease(fixture, visibilityOptions(3, delays, progress)),
|
|
139
|
-
/npm publish succeeded but package was not visible after 3 npm read attempts/,
|
|
140
|
-
);
|
|
141
|
-
assert.deepEqual(delays, [10, 20]);
|
|
142
|
-
assert.deepEqual(progress, [
|
|
143
|
-
'npm visibility: attempt 1/3',
|
|
144
|
-
'npm visibility: attempt 2/3',
|
|
145
|
-
'npm visibility: attempt 3/3',
|
|
146
|
-
]);
|
|
147
|
-
assert.equal(fixture.events.filter((event) => event === 'read npm').length, 4);
|
|
148
|
-
assert.ok(!fixture.events.includes('create github'));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('permanent GitHub absence fails at the bound with post-create phase evidence', async () => {
|
|
152
|
-
const fixture = adapter({ npmPublished: true, githubInvisibleReads: 4 });
|
|
153
|
-
const delays = [];
|
|
154
|
-
await assert.rejects(
|
|
155
|
-
reconcileRelease(fixture, visibilityOptions(3, delays)),
|
|
156
|
-
/GitHub release creation succeeded but release was not visible after 3 GitHub read attempts/,
|
|
157
|
-
);
|
|
158
|
-
assert.deepEqual(delays, [10, 20]);
|
|
159
|
-
assert.equal(fixture.events.filter((event) => event === 'read github').length, 4);
|
|
160
|
-
assert.ok(!fixture.events.includes('publish npm'));
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('a rejected publish is not reported as a post-publish visibility timeout', async () => {
|
|
164
|
-
const rejected = new Error('registry rejected publish');
|
|
165
|
-
const fixture = adapter({ publishNpmError: rejected });
|
|
166
|
-
await assert.rejects(reconcileRelease(fixture, visibilityOptions(3)), (error) => error === rejected);
|
|
167
|
-
assert.deepEqual(fixture.events, ['read npm', 'read github', 'publish npm']);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test('a rejected GitHub create is not reported as a post-create visibility timeout', async () => {
|
|
171
|
-
const rejected = new Error('GitHub rejected release create');
|
|
172
|
-
const fixture = adapter({ npmPublished: true, createGithubError: rejected });
|
|
173
|
-
await assert.rejects(reconcileRelease(fixture, visibilityOptions(3)), (error) => error === rejected);
|
|
174
|
-
assert.deepEqual(fixture.events, ['read npm', 'read github', 'create github']);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test('an npm-published release resumes at GitHub without a second npm publish', async () => {
|
|
178
|
-
const fixture = adapter({ npmPublished: true });
|
|
179
|
-
await reconcileRelease(fixture);
|
|
180
|
-
assert.deepEqual(fixture.events, ['read npm', 'read github', 'create github', 'read github']);
|
|
181
|
-
assert.ok(!fixture.events.includes('publish npm'));
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('a rerun after eventual npm visibility never publishes the version twice', async () => {
|
|
185
|
-
const fixture = adapter({ npmInvisibleReads: 2 });
|
|
186
|
-
await reconcileRelease(fixture, visibilityOptions(4));
|
|
187
|
-
await reconcileRelease(fixture, visibilityOptions(4));
|
|
188
|
-
assert.equal(fixture.events.filter((event) => event === 'publish npm').length, 1);
|
|
189
|
-
assert.equal(fixture.events.filter((event) => event === 'create github').length, 1);
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test('an already-published release at parity is a read-only no-op', async () => {
|
|
193
|
-
const fixture = adapter({ npmPublished: true, githubReleased: true });
|
|
194
|
-
assert.deepEqual(await reconcileRelease(fixture), { status: 'released', identity });
|
|
195
|
-
assert.deepEqual(fixture.events, ['read npm', 'read github']);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
test('post-merge status inspection is read-only and reports the reconstructable phase', async () => {
|
|
199
|
-
const fixture = adapter({ npmPublished: true });
|
|
200
|
-
assert.deepEqual(await inspectRelease(fixture), { status: 'awaiting-github', identity });
|
|
201
|
-
assert.deepEqual(fixture.events, ['read npm', 'read github']);
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
test('the registry read bypasses the local packument cache, the local pack does not', async () => {
|
|
205
|
-
const calls = [];
|
|
206
|
-
const run = async (command, args) => {
|
|
207
|
-
calls.push({ command, args });
|
|
208
|
-
throw Object.assign(new Error('stubbed pack'), { stderr: 'stubbed pack' });
|
|
209
|
-
};
|
|
210
|
-
const probe = await createCommandAdapter({ repoRoot: REPO, run });
|
|
211
|
-
try {
|
|
212
|
-
await assert.rejects(() => probe.local());
|
|
213
|
-
await assert.rejects(() => probe.npm(identity));
|
|
214
|
-
} finally { await probe.dispose(); }
|
|
215
|
-
|
|
216
|
-
const [localPack, registryPack] = calls;
|
|
217
|
-
// A stale packument makes `npm pack <name>@<version>` answer ETARGET for a
|
|
218
|
-
// version that IS published — the status would then claim awaiting-npm and
|
|
219
|
-
// invite a second publish. The local pack must NOT reach for the registry.
|
|
220
|
-
assert.ok(registryPack.args.includes('--prefer-online'), 'registry read must bypass the cache');
|
|
221
|
-
assert.ok(registryPack.args.includes(`${identity.name}@${identity.version}`));
|
|
222
|
-
assert.ok(!localPack.args.includes('--prefer-online'), 'local pack must stay offline');
|
|
223
|
-
assert.ok(localPack.args.includes('.'));
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
test('local Claude overrides cannot change the packed release identity or enter the tarball', async () => {
|
|
227
|
-
const fixture = await mkdtemp(join(tmpdir(), 'awkit-local-overrides-'));
|
|
228
|
-
await mkdir(join(fixture, '.claude'), { recursive: true });
|
|
229
|
-
await Promise.all([
|
|
230
|
-
['package.json', 'package.json'],
|
|
231
|
-
['agent-workflow-kit.package.json', 'agent-workflow-kit.package.json'],
|
|
232
|
-
['.claude/.npmignore', '.claude/.npmignore'],
|
|
233
|
-
].map(([source, destination]) => copyFile(join(REPO, source), join(fixture, destination))));
|
|
234
|
-
await writeFile(join(fixture, '.claude/shipped.json'), '{"shipped":true}\n');
|
|
235
|
-
|
|
236
|
-
const overrides = [
|
|
237
|
-
['.claude/settings.local.json', randomUUID()],
|
|
238
|
-
['.claude/settings.local.json.backup', randomUUID()],
|
|
239
|
-
];
|
|
240
|
-
|
|
241
|
-
const clean = await createCommandAdapter({ repoRoot: fixture });
|
|
242
|
-
const local = await createCommandAdapter({ repoRoot: fixture });
|
|
243
|
-
try {
|
|
244
|
-
const { identity: publishedIdentity } = await clean.local();
|
|
245
|
-
for (const [path, canary] of overrides) {
|
|
246
|
-
await writeFile(join(fixture, path), `${canary}\n`);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
const { identity, tarball } = await local.local();
|
|
250
|
-
assert.deepEqual(identity, publishedIdentity);
|
|
251
|
-
assert.deepEqual(await inspectRelease({
|
|
252
|
-
local: async () => ({ identity, tarball }),
|
|
253
|
-
npm: async () => publishedIdentity,
|
|
254
|
-
github: async () => publishedIdentity,
|
|
255
|
-
}), { status: 'released', identity: publishedIdentity });
|
|
256
|
-
|
|
257
|
-
const paths = new Set(
|
|
258
|
-
execFileSync('tar', ['-tzf', tarball], { encoding: 'utf8' })
|
|
259
|
-
.trim().split('\n').map((path) => path.replace(/^package\//, '')),
|
|
260
|
-
);
|
|
261
|
-
assert.ok(paths.has('.claude/shipped.json'));
|
|
262
|
-
const contents = execFileSync('tar', ['-xOzf', tarball], {
|
|
263
|
-
encoding: 'utf8', maxBuffer: 16 * 1024 * 1024,
|
|
264
|
-
});
|
|
265
|
-
for (const [path, canary] of overrides) {
|
|
266
|
-
assert.ok(!paths.has(path), `packed local override: ${path}`);
|
|
267
|
-
assert.doesNotMatch(contents, new RegExp(canary));
|
|
268
|
-
}
|
|
269
|
-
} finally {
|
|
270
|
-
await Promise.all([clean.dispose(), local.dispose()]);
|
|
271
|
-
await rm(fixture, { recursive: true, force: true });
|
|
272
|
-
}
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('an unpublished npm version is reconstructable when npm reports ETARGET', () => {
|
|
276
|
-
assert.equal(isMissingRelease({
|
|
277
|
-
stderr: 'npm error code ETARGET\nnpm error notarget No matching version found',
|
|
278
|
-
}, 'npm'), true);
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
test('a mismatching npm package blocks GitHub release creation', async () => {
|
|
282
|
-
const fixture = adapter({
|
|
283
|
-
npmPublished: true,
|
|
284
|
-
npmIdentity: { ...identity, manifestSha256: 'wrong' },
|
|
285
|
-
});
|
|
286
|
-
await assert.rejects(reconcileRelease(fixture), /npm manifestSha256 mismatch/);
|
|
287
|
-
assert.ok(!fixture.events.includes('create github'));
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('a post-publish npm mismatch hard-stops without retrying or creating GitHub', async () => {
|
|
291
|
-
const fixture = adapter({
|
|
292
|
-
npmIdentity: { ...identity, manifestSha256: 'wrong' },
|
|
293
|
-
});
|
|
294
|
-
const delays = [];
|
|
295
|
-
const progress = [];
|
|
296
|
-
await assert.rejects(
|
|
297
|
-
reconcileRelease(fixture, visibilityOptions(4, delays, progress)),
|
|
298
|
-
/npm manifestSha256 mismatch/,
|
|
299
|
-
);
|
|
300
|
-
assert.deepEqual(delays, []);
|
|
301
|
-
assert.deepEqual(progress, ['npm visibility: attempt 1/4']);
|
|
302
|
-
assert.equal(fixture.events.filter((event) => event === 'publish npm').length, 1);
|
|
303
|
-
assert.equal(fixture.events.filter((event) => event === 'read npm').length, 2);
|
|
304
|
-
assert.ok(!fixture.events.includes('create github'));
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('a post-publish non-missing registry error hard-stops without retrying', async () => {
|
|
308
|
-
const registryError = new Error('registry authentication failed');
|
|
309
|
-
const fixture = adapter({ npmReadErrorAfterPublish: registryError });
|
|
310
|
-
const delays = [];
|
|
311
|
-
const progress = [];
|
|
312
|
-
await assert.rejects(
|
|
313
|
-
reconcileRelease(fixture, visibilityOptions(4, delays, progress)),
|
|
314
|
-
(error) => error === registryError,
|
|
315
|
-
);
|
|
316
|
-
assert.deepEqual(delays, []);
|
|
317
|
-
assert.deepEqual(progress, ['npm visibility: attempt 1/4']);
|
|
318
|
-
assert.equal(fixture.events.filter((event) => event === 'publish npm').length, 1);
|
|
319
|
-
assert.ok(!fixture.events.includes('create github'));
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test('release workflow uses supported OIDC trusted publishing without a long-lived npm token', async () => {
|
|
323
|
-
const workflow = await readFile(join(REPO, '.github/workflows/release.yml'), 'utf8');
|
|
324
|
-
assert.match(workflow, /node-version:\s*22\.14/);
|
|
325
|
-
assert.match(workflow, /npm@11\.5\.1/);
|
|
326
|
-
assert.match(workflow, /id-token:\s*write/);
|
|
327
|
-
assert.match(workflow, /contents:\s*write/);
|
|
328
|
-
assert.match(workflow, /registry-url:\s*['"]https:\/\/registry\.npmjs\.org['"]/);
|
|
329
|
-
assert.match(workflow, /node scripts\/release-state\.mjs/);
|
|
330
|
-
assert.doesNotMatch(workflow, /NPM_TOKEN|NODE_AUTH_TOKEN|npm_[A-Za-z0-9]/);
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
test('the production publisher explicitly requests npm provenance', async () => {
|
|
334
|
-
const commands = [];
|
|
335
|
-
const fixture = await createCommandAdapter({
|
|
336
|
-
repoRoot: REPO,
|
|
337
|
-
run: async (command, args) => { commands.push([command, ...args]); return { stdout: '' }; },
|
|
338
|
-
});
|
|
339
|
-
try {
|
|
340
|
-
await fixture.publishNpm({ tarball: '/tmp/kit.tgz' });
|
|
341
|
-
assert.deepEqual(commands, [[
|
|
342
|
-
'npm', 'publish', '/tmp/kit.tgz', '--access', 'public', '--provenance',
|
|
343
|
-
]]);
|
|
344
|
-
} finally { await fixture.dispose(); }
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
test('an incomplete GitHub release resumes by uploading the verified registry tarball', () => {
|
|
348
|
-
assert.deepEqual(githubReleaseArgs({
|
|
349
|
-
exists: true, tag: 'v1.2.3', tarball: '/tmp/ikon85-agent-workflow-kit-1.2.3.tgz', target: 'abc123',
|
|
350
|
-
}), [
|
|
351
|
-
'release', 'upload', 'v1.2.3', '/tmp/ikon85-agent-workflow-kit-1.2.3.tgz', '--clobber',
|
|
352
|
-
]);
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
test('a scoped npm identity maps to the registry tarball asset name', () => {
|
|
356
|
-
assert.equal(
|
|
357
|
-
npmTarballFilename('@ikon85/agent-workflow-kit', '1.2.3'),
|
|
358
|
-
'ikon85-agent-workflow-kit-1.2.3.tgz',
|
|
359
|
-
);
|
|
360
|
-
});
|