mandrel 1.59.0 → 1.61.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/README.md +86 -44
- package/.agents/docs/SDLC.md +135 -141
- package/.agents/docs/configuration.md +77 -20
- package/.agents/docs/quality-gates.md +796 -0
- package/.agents/docs/workflows.md +6 -9
- package/.agents/instructions.md +12 -11
- package/.agents/personas/architect.md +1 -1
- package/.agents/personas/product.md +1 -1
- package/.agents/personas/project-manager.md +14 -14
- package/.agents/personas/technical-writer.md +1 -1
- package/.agents/rules/changelog-style.md +5 -5
- package/.agents/rules/git-conventions.md +3 -3
- package/.agents/runtime-deps.json +2 -2
- package/.agents/schemas/agentrc.schema.json +3 -3
- package/.agents/schemas/dispatch-manifest.json +4 -4
- package/.agents/schemas/epic-spec.schema.json +15 -45
- package/.agents/schemas/lifecycle/README.md +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.end.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.dispatch.start.schema.json +1 -1
- package/.agents/schemas/lifecycle/story.heartbeat.schema.json +1 -1
- package/.agents/schemas/validation-evidence.schema.json +1 -1
- package/.agents/scripts/README.md +2 -2
- package/.agents/scripts/acceptance-eval.js +1 -1
- package/.agents/scripts/acceptance-spec-reconciler.js +2 -2
- package/.agents/scripts/agents-bootstrap-github.js +23 -119
- package/.agents/scripts/analyze-execution.js +2 -2
- package/.agents/scripts/audit-to-stories.js +1 -1
- package/.agents/scripts/check-doc-links.js +2 -3
- package/.agents/scripts/diagnose-friction.js +1 -1
- package/.agents/scripts/dispatcher.js +2 -2
- package/.agents/scripts/drain-pending-cleanup.js +1 -1
- package/.agents/scripts/epic-audit-prepare.js +3 -3
- package/.agents/scripts/epic-deliver-note-intervention.js +2 -2
- package/.agents/scripts/epic-deliver-preflight.js +6 -6
- package/.agents/scripts/epic-deliver-prepare.js +1 -1
- package/.agents/scripts/epic-execute-record-wave.js +4 -4
- package/.agents/scripts/epic-plan-healthcheck.js +6 -10
- package/.agents/scripts/epic-plan-spec-validate.js +1 -1
- package/.agents/scripts/epic-reconcile.js +11 -29
- package/.agents/scripts/evidence-gate.js +1 -1
- package/.agents/scripts/generate-workflows-doc.js +1 -1
- package/.agents/scripts/hierarchy-gate.js +7 -11
- package/.agents/scripts/lib/ITicketingProvider.js +1 -1
- package/.agents/scripts/lib/audit-suite/selector.js +1 -1
- package/.agents/scripts/lib/audit-to-stories/seed-epic-from-findings.js +2 -2
- package/.agents/scripts/lib/baseline-snapshot.js +7 -7
- package/.agents/scripts/lib/bdd-runner-detect.js +1 -1
- package/.agents/scripts/lib/bdd-scenario-scanner.js +3 -3
- package/.agents/scripts/lib/bootstrap/baselines-layout-migration.js +1 -1
- package/.agents/scripts/lib/bootstrap/branch-protection.js +1 -1
- package/.agents/scripts/lib/bootstrap/ci-workflow-template.js +47 -1
- package/.agents/scripts/lib/bootstrap/commit-push.js +2 -2
- package/.agents/scripts/lib/bootstrap/gh-preflight.js +7 -9
- package/.agents/scripts/lib/bootstrap/manifest.js +21 -1
- package/.agents/scripts/lib/bootstrap/merge-methods.js +31 -16
- package/.agents/scripts/lib/bootstrap/project-bootstrap.js +32 -11
- package/.agents/scripts/lib/codebase-snapshot.js +1 -1
- package/.agents/scripts/lib/config/explain.js +1 -1
- package/.agents/scripts/lib/config/runners.js +2 -2
- package/.agents/scripts/lib/config/runtime.js +1 -1
- package/.agents/scripts/lib/config/sync-agentrc.js +1 -1
- package/.agents/scripts/lib/config/temp-paths.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-delivery.js +2 -2
- package/.agents/scripts/lib/config-settings-schema-quality.js +1 -1
- package/.agents/scripts/lib/config-settings-schema.js +3 -3
- package/.agents/scripts/lib/detect-package-manager.js +72 -0
- package/.agents/scripts/lib/duplicate-search.js +1 -1
- package/.agents/scripts/lib/dynamic-workflow/capability.js +1 -1
- package/.agents/scripts/lib/epic-plan-clarity.js +1 -1
- package/.agents/scripts/lib/epic-plan-ideation.js +1 -1
- package/.agents/scripts/lib/errors/index.js +4 -4
- package/.agents/scripts/lib/feedback-loop/memory-freshness.js +1 -1
- package/.agents/scripts/lib/feedback-loop/prior-feedback-fetcher.js +1 -1
- package/.agents/scripts/lib/findings/classify-finding.js +1 -1
- package/.agents/scripts/lib/findings/promote-finding.js +10 -10
- package/.agents/scripts/lib/label-constants.js +3 -4
- package/.agents/scripts/lib/label-taxonomy.js +5 -10
- package/.agents/scripts/lib/onboard/detect-stack.js +10 -10
- package/.agents/scripts/lib/onboard/init-tail.js +218 -0
- package/.agents/scripts/lib/onboard/scaffold-docs.js +18 -3
- package/.agents/scripts/lib/orchestration/acceptance-eval-decision.js +1 -1
- package/.agents/scripts/lib/orchestration/code-review.js +5 -5
- package/.agents/scripts/lib/orchestration/context-hydration-engine.js +8 -9
- package/.agents/scripts/lib/orchestration/dependency-analyzer.js +3 -3
- package/.agents/scripts/lib/orchestration/detectors-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/dispatch-engine.js +30 -38
- package/.agents/scripts/lib/orchestration/dispatch-pipeline.js +9 -25
- package/.agents/scripts/lib/orchestration/epic-cleanup.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-deliver-lease-guard.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/creation.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/dag.js +7 -21
- package/.agents/scripts/lib/orchestration/epic-plan-decompose/phases/diagnostics.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-plan-lease-guard.js +26 -13
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/plan-epic.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/prompts.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-plan-spec/phases/run-spec-phase.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-plan-state-store.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-run-state-store.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/concurrency-gate.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/deliver-phases.js +3 -3
- package/.agents/scripts/lib/orchestration/epic-runner/phases/build-wave-dag.js +6 -21
- package/.agents/scripts/lib/orchestration/epic-runner/phases/snapshot.js +7 -7
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/composition.js +1 -1
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/signals.js +2 -2
- package/.agents/scripts/lib/orchestration/epic-runner/progress-reporter/transport.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-launcher.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-runner/story-run-progress-writer.js +8 -8
- package/.agents/scripts/lib/orchestration/epic-runner/sub-agent-return.js +4 -4
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-apply.js +7 -15
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-diff.js +72 -41
- package/.agents/scripts/lib/orchestration/epic-spec-reconciler-ops.js +2 -4
- package/.agents/scripts/lib/orchestration/file-assumptions.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/close-planning-tickets.js +1 -1
- package/.agents/scripts/lib/orchestration/finalize/open-or-locate-pr.js +2 -2
- package/.agents/scripts/lib/orchestration/finalize/sanitize-skip-ci.js +1 -1
- package/.agents/scripts/lib/orchestration/lease-guard-shared.js +3 -3
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-dispatch-end.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/emit-story-heartbeat.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/README.md +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-armer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/automerge-predicate.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/branch-cleaner.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/finalizer.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/index.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/merge-watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/notify-dispatcher.js +1 -1
- package/.agents/scripts/lib/orchestration/lifecycle/listeners/watcher.js +1 -1
- package/.agents/scripts/lib/orchestration/manifest-builder.js +5 -5
- package/.agents/scripts/lib/orchestration/parked-follow-ons.js +2 -2
- package/.agents/scripts/lib/orchestration/plan-runner/plan-router.js +5 -5
- package/.agents/scripts/lib/orchestration/post-merge/phases/ticket-closure.js +3 -3
- package/.agents/scripts/lib/orchestration/preflight-cache.js +1 -1
- package/.agents/scripts/lib/orchestration/recurring-failure-detector.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/compose-body.js +1 -1
- package/.agents/scripts/lib/orchestration/retro/phases/gather-signals.js +2 -2
- package/.agents/scripts/lib/orchestration/retro-runner.js +3 -3
- package/.agents/scripts/lib/orchestration/review-depth.js +1 -1
- package/.agents/scripts/lib/orchestration/single-story-close/phases/wrong-tree-guard.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-freshness.js +1 -1
- package/.agents/scripts/lib/orchestration/spec-renderer.js +36 -73
- package/.agents/scripts/lib/orchestration/spec-section-validator.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/baseline-friction-body.js +1 -1
- package/.agents/scripts/lib/orchestration/story-close/phases/locked-pipeline.js +2 -2
- package/.agents/scripts/lib/orchestration/task-body-validator.js +6 -6
- package/.agents/scripts/lib/orchestration/ticket-lease.js +1 -1
- package/.agents/scripts/lib/orchestration/ticket-validator-conflicts.js +2 -2
- package/.agents/scripts/lib/orchestration/ticket-validator-sizing.js +1 -10
- package/.agents/scripts/lib/orchestration/ticket-validator.js +25 -70
- package/.agents/scripts/lib/orchestration/ticketing/bulk.js +5 -12
- package/.agents/scripts/lib/orchestration/ticketing/reads.js +8 -8
- package/.agents/scripts/lib/orchestration/ticketing/state.js +3 -3
- package/.agents/scripts/lib/orchestration/wave-record-notifications.js +2 -2
- package/.agents/scripts/lib/orchestration/wave-record-projection.js +1 -1
- package/.agents/scripts/lib/plan-phase-cleanup.js +1 -1
- package/.agents/scripts/lib/preflight-runner.js +1 -1
- package/.agents/scripts/lib/presentation/dispatch-manifest-render.js +4 -5
- package/.agents/scripts/lib/presentation/manifest-builder.js +28 -34
- package/.agents/scripts/lib/presentation/manifest-formatter.js +3 -4
- package/.agents/scripts/lib/presentation/manifest-helpers.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-procedures.js +4 -4
- package/.agents/scripts/lib/presentation/manifest-render-waves.js +4 -23
- package/.agents/scripts/lib/presentation/manifest-renderer.js +1 -1
- package/.agents/scripts/lib/presentation/manifest-story-views.js +2 -11
- package/.agents/scripts/lib/runtime-deps/preflight.js +6 -6
- package/.agents/scripts/lib/signals/schema.js +1 -1
- package/.agents/scripts/lib/spec/index.js +1 -1
- package/.agents/scripts/lib/spec/loader.js +2 -2
- package/.agents/scripts/lib/spec/state.js +7 -16
- package/.agents/scripts/lib/story-init/context-resolver.js +3 -3
- package/.agents/scripts/lib/story-init/state-transitioner.js +2 -2
- package/.agents/scripts/lib/story-init/task-graph-builder.js +7 -7
- package/.agents/scripts/lib/story-lifecycle.js +8 -8
- package/.agents/scripts/lib/story-plan.js +1 -1
- package/.agents/scripts/lib/templates/decomposer-prompts.js +59 -52
- package/.agents/scripts/lib/wave-runner/tick.js +1 -1
- package/.agents/scripts/lib/worktree/node-modules-strategy.js +5 -2
- package/.agents/scripts/lifecycle-emit-story-dispatch.js +1 -1
- package/.agents/scripts/lifecycle-emit.js +1 -1
- package/.agents/scripts/providers/github/board-add.js +1 -1
- package/.agents/scripts/providers/github/errors.js +1 -1
- package/.agents/scripts/providers/github/mappers.js +2 -2
- package/.agents/scripts/providers/github/tickets.js +4 -4
- package/.agents/scripts/resync-status-column.js +1 -1
- package/.agents/scripts/retro-run.js +2 -2
- package/.agents/scripts/run-lint.js +1 -1
- package/.agents/scripts/single-story-init.js +1 -1
- package/.agents/scripts/stories-wave-tick.js +5 -5
- package/.agents/scripts/story-close.js +1 -1
- package/.agents/scripts/story-init.js +13 -16
- package/.agents/scripts/story-phase.js +5 -5
- package/.agents/scripts/story-plan.js +3 -3
- package/.agents/scripts/sync-branch-from-base.js +1 -1
- package/.agents/scripts/validate-docs-freshness.js +1 -1
- package/.agents/scripts/wave-tick.js +1 -1
- package/.agents/skills/core/analyze-execution/SKILL.md +2 -2
- package/.agents/skills/core/epic-plan-consolidate/SKILL.md +21 -26
- package/.agents/skills/core/epic-plan-decompose-author/SKILL.md +23 -56
- package/.agents/skills/core/epic-plan-spec-author/SKILL.md +4 -4
- package/.agents/skills/core/hydrate-context/SKILL.md +2 -2
- package/.agents/skills/core/idea-refinement/SKILL.md +4 -4
- package/.agents/skills/core/knowledge-transfer/SKILL.md +2 -2
- package/.agents/skills/core/planning-and-task-breakdown/SKILL.md +1 -1
- package/.agents/skills/core/scope-triage/SKILL.md +9 -10
- package/.agents/skills/core/using-agent-skills/SKILL.md +1 -1
- package/.agents/skills/skills.index.json +7 -7
- package/.agents/templates/agent-protocol.md +2 -2
- package/.agents/workflows/agents-update.md +16 -31
- package/.agents/workflows/audit-architecture.md +2 -2
- package/.agents/workflows/audit-clean-code.md +2 -2
- package/.agents/workflows/audit-dependencies.md +1 -1
- package/.agents/workflows/audit-devops.md +1 -1
- package/.agents/workflows/audit-documentation.md +2 -2
- package/.agents/workflows/audit-lighthouse.md +1 -1
- package/.agents/workflows/audit-performance.md +2 -2
- package/.agents/workflows/audit-privacy.md +1 -1
- package/.agents/workflows/audit-quality.md +2 -2
- package/.agents/workflows/audit-security.md +2 -2
- package/.agents/workflows/audit-seo.md +1 -1
- package/.agents/workflows/audit-sre.md +1 -1
- package/.agents/workflows/audit-to-stories.md +10 -10
- package/.agents/workflows/audit-ux-ui.md +1 -1
- package/.agents/workflows/deliver.md +85 -0
- package/.agents/workflows/explain.md +3 -3
- package/.agents/workflows/git-merge-pr.md +1 -1
- package/.agents/workflows/git-pr-all.md +13 -10
- package/.agents/workflows/git-push.md +6 -3
- package/.agents/workflows/helpers/_merge-conflict-template.md +1 -1
- package/.agents/workflows/helpers/acceptance-self-eval.md +1 -1
- package/.agents/workflows/helpers/agents-sync-config.md +3 -2
- package/.agents/workflows/helpers/code-review.md +5 -5
- package/.agents/workflows/{epic-deliver.md → helpers/deliver-epic.md} +43 -43
- package/.agents/workflows/{story-deliver.md → helpers/deliver-stories.md} +25 -25
- package/.agents/workflows/helpers/diagnose.md +1 -1
- package/.agents/workflows/helpers/epic-audit.md +6 -6
- package/.agents/workflows/helpers/epic-deliver-story.md +13 -13
- package/.agents/workflows/helpers/epic-plan-decompose.md +23 -23
- package/.agents/workflows/helpers/epic-plan-spec.md +6 -6
- package/.agents/workflows/helpers/epic-testing.md +3 -3
- package/.agents/workflows/helpers/parallel-tooling.md +1 -1
- package/.agents/workflows/{epic-plan.md → helpers/plan-epic.md} +84 -84
- package/.agents/workflows/{story-plan.md → helpers/plan-story.md} +43 -43
- package/.agents/workflows/helpers/signals.md +1 -1
- package/.agents/workflows/helpers/single-story-deliver.md +11 -11
- package/.agents/workflows/helpers/worktree-lifecycle.md +18 -18
- package/.agents/workflows/plan.md +131 -0
- package/.agents/workflows/qa-explore.md +1 -1
- package/.agents/workflows/qa-run-harness.md +1 -1
- package/README.md +19 -39
- package/bin/mandrel.js +235 -16
- package/docs/CHANGELOG.md +1173 -0
- package/lib/cli/doctor.js +45 -3
- package/lib/cli/init.js +97 -36
- package/lib/cli/registry.js +41 -145
- package/lib/cli/sync.js +122 -23
- package/lib/cli/uninstall.js +42 -7
- package/lib/cli/update.js +524 -210
- package/lib/cli/version-helpers.js +59 -0
- package/package.json +7 -6
- package/.agents/scripts/lib/orchestration/reconciler.js +0 -137
- package/.agents/workflows/onboard.md +0 -208
- package/lib/cli/__tests__/migrate.test.js +0 -268
- package/lib/cli/__tests__/sync-local-zone.test.js +0 -247
- package/lib/cli/__tests__/sync.test.js +0 -372
- package/lib/cli/__tests__/update-major.test.js +0 -217
- package/lib/cli/__tests__/update.test.js +0 -696
- package/lib/cli/__tests__/version-check.test.js +0 -398
- package/lib/migrations/__tests__/index.test.js +0 -216
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
// lib/cli/__tests__/migrate.test.js
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for lib/cli/migrate.js — the standalone `mandrel migrate`
|
|
4
|
-
* subcommand (Story #3505, Epic #3437).
|
|
5
|
-
*
|
|
6
|
-
* Every test drives runMigrate through injectable seams (argv, runMigrations,
|
|
7
|
-
* registry, ctx, write, writeErr, exit). No real filesystem I/O, no real
|
|
8
|
-
* network call, and no shared mutable module state occur (testing-standards
|
|
9
|
-
* § Unit: all external I/O MUST be mocked; pure-logic assertions only).
|
|
10
|
-
*
|
|
11
|
-
* Coverage contract (Story #3505 AC):
|
|
12
|
-
* - Module shape: runMigrate named export + default function export.
|
|
13
|
-
* - A live run parses --from/--to out of argv and forwards them to
|
|
14
|
-
* runMigrations.
|
|
15
|
-
* - --dry-run reports the steps that WOULD run and invokes no step's apply
|
|
16
|
-
* and no runMigrations call (writes nothing to disk).
|
|
17
|
-
* - Missing --from or --to is a usage error (non-zero exit).
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import assert from 'node:assert/strict';
|
|
21
|
-
import { describe, it } from 'node:test';
|
|
22
|
-
|
|
23
|
-
import migrate, { runMigrate } from '../migrate.js';
|
|
24
|
-
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
// Capture + seam helpers
|
|
27
|
-
// ---------------------------------------------------------------------------
|
|
28
|
-
|
|
29
|
-
/** Capture stdout/stderr writes and the exit code. */
|
|
30
|
-
function makeCapture() {
|
|
31
|
-
const out = [];
|
|
32
|
-
const err = [];
|
|
33
|
-
let exitCode = null;
|
|
34
|
-
return {
|
|
35
|
-
out,
|
|
36
|
-
err,
|
|
37
|
-
get exitCode() {
|
|
38
|
-
return exitCode;
|
|
39
|
-
},
|
|
40
|
-
write: (s) => out.push(s),
|
|
41
|
-
writeErr: (s) => err.push(s),
|
|
42
|
-
exit: (code) => {
|
|
43
|
-
exitCode = code;
|
|
44
|
-
},
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Build a fixture registry with `detect`/`apply` recorders so the dry-run
|
|
50
|
-
* (detect only, no apply) and live (detect → apply) paths are both
|
|
51
|
-
* observable. `appliedNeeded` lists the versions whose detect returns true
|
|
52
|
-
* (still needs applying); every other step is treated as already-present.
|
|
53
|
-
*/
|
|
54
|
-
function makeFixtureRegistry({ appliedNeeded = ['1.4.0', '1.5.0'] } = {}) {
|
|
55
|
-
const calls = [];
|
|
56
|
-
const registry = [
|
|
57
|
-
{ version: '1.3.0', description: 'pre-range step' },
|
|
58
|
-
{ version: '1.4.0', description: 'rename foo to bar' },
|
|
59
|
-
{ version: '1.5.0', description: 'move baseline file' },
|
|
60
|
-
{ version: '1.6.0', description: 'post-range step' },
|
|
61
|
-
].map((step) => ({
|
|
62
|
-
...step,
|
|
63
|
-
detect: (_ctx) => {
|
|
64
|
-
calls.push(`detect:${step.version}`);
|
|
65
|
-
return appliedNeeded.includes(step.version);
|
|
66
|
-
},
|
|
67
|
-
apply: (_ctx) => {
|
|
68
|
-
calls.push(`apply:${step.version}`);
|
|
69
|
-
},
|
|
70
|
-
}));
|
|
71
|
-
return { registry, calls };
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// ---------------------------------------------------------------------------
|
|
75
|
-
// Module shape
|
|
76
|
-
// ---------------------------------------------------------------------------
|
|
77
|
-
|
|
78
|
-
describe('migrate module exports', () => {
|
|
79
|
-
it('exports runMigrate as a named export', () => {
|
|
80
|
-
assert.equal(typeof runMigrate, 'function');
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('exports a default function for bin/mandrel.js dispatch', () => {
|
|
84
|
-
assert.equal(typeof migrate, 'function');
|
|
85
|
-
});
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// ---------------------------------------------------------------------------
|
|
89
|
-
// AC — live run forwards parsed --from/--to to runMigrations
|
|
90
|
-
// ---------------------------------------------------------------------------
|
|
91
|
-
|
|
92
|
-
describe('runMigrate — live run', () => {
|
|
93
|
-
it('parses --from/--to and forwards them to runMigrations', () => {
|
|
94
|
-
const cap = makeCapture();
|
|
95
|
-
const calls = [];
|
|
96
|
-
const runMigrations = ({ fromVersion, toVersion }) => {
|
|
97
|
-
calls.push(`runMigrations:${fromVersion}->${toVersion}`);
|
|
98
|
-
return { applied: ['1.4.0'], skipped: [] };
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const result = runMigrate({
|
|
102
|
-
argv: ['--from', '1.3.0', '--to', '1.5.0'],
|
|
103
|
-
runMigrations,
|
|
104
|
-
registry: [],
|
|
105
|
-
write: cap.write,
|
|
106
|
-
writeErr: cap.writeErr,
|
|
107
|
-
exit: cap.exit,
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
assert.deepEqual(calls, ['runMigrations:1.3.0->1.5.0']);
|
|
111
|
-
assert.equal(result.ok, true);
|
|
112
|
-
assert.equal(result.action, 'migrated');
|
|
113
|
-
assert.deepEqual(result.applied, ['1.4.0']);
|
|
114
|
-
assert.equal(cap.exitCode, null);
|
|
115
|
-
assert.match(cap.out.join(''), /Applied 1 migration/);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('accepts the --from=value / --to=value spelling', () => {
|
|
119
|
-
const cap = makeCapture();
|
|
120
|
-
const calls = [];
|
|
121
|
-
const runMigrations = ({ fromVersion, toVersion }) => {
|
|
122
|
-
calls.push(`runMigrations:${fromVersion}->${toVersion}`);
|
|
123
|
-
return { applied: [], skipped: [] };
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
runMigrate({
|
|
127
|
-
argv: ['--from=1.2.0', '--to=1.9.0'],
|
|
128
|
-
runMigrations,
|
|
129
|
-
registry: [],
|
|
130
|
-
write: cap.write,
|
|
131
|
-
writeErr: cap.writeErr,
|
|
132
|
-
exit: cap.exit,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
assert.deepEqual(calls, ['runMigrations:1.2.0->1.9.0']);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('reports a no-op when no migrations applied', () => {
|
|
139
|
-
const cap = makeCapture();
|
|
140
|
-
const runMigrations = () => ({ applied: [], skipped: ['1.4.0'] });
|
|
141
|
-
|
|
142
|
-
const result = runMigrate({
|
|
143
|
-
argv: ['--from', '1.3.0', '--to', '1.5.0'],
|
|
144
|
-
runMigrations,
|
|
145
|
-
registry: [],
|
|
146
|
-
write: cap.write,
|
|
147
|
-
writeErr: cap.writeErr,
|
|
148
|
-
exit: cap.exit,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
assert.equal(result.ok, true);
|
|
152
|
-
assert.deepEqual(result.applied, []);
|
|
153
|
-
assert.match(cap.out.join(''), /no migrations to apply/);
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ---------------------------------------------------------------------------
|
|
158
|
-
// AC — --dry-run reports the plan, invokes no apply, writes nothing
|
|
159
|
-
// ---------------------------------------------------------------------------
|
|
160
|
-
|
|
161
|
-
describe('runMigrate — --dry-run', () => {
|
|
162
|
-
it('reports the in-range steps that would apply / be skipped and never applies', () => {
|
|
163
|
-
const cap = makeCapture();
|
|
164
|
-
const { registry, calls } = makeFixtureRegistry({
|
|
165
|
-
// 1.5.0 is in range but already present (detect → false ⇒ would skip).
|
|
166
|
-
appliedNeeded: ['1.4.0'],
|
|
167
|
-
});
|
|
168
|
-
let runnerCalled = false;
|
|
169
|
-
|
|
170
|
-
const result = runMigrate({
|
|
171
|
-
argv: ['--from', '1.3.0', '--to', '1.5.0', '--dry-run'],
|
|
172
|
-
runMigrations: () => {
|
|
173
|
-
runnerCalled = true;
|
|
174
|
-
return { applied: [], skipped: [] };
|
|
175
|
-
},
|
|
176
|
-
registry,
|
|
177
|
-
write: cap.write,
|
|
178
|
-
writeErr: cap.writeErr,
|
|
179
|
-
exit: cap.exit,
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
// Range filter is fromVersion < v <= toVersion → only 1.4.0 and 1.5.0.
|
|
183
|
-
assert.deepEqual(result.wouldApply, ['1.4.0']);
|
|
184
|
-
assert.deepEqual(result.wouldSkip, ['1.5.0']);
|
|
185
|
-
assert.equal(result.action, 'dry-run');
|
|
186
|
-
assert.equal(result.ok, true);
|
|
187
|
-
|
|
188
|
-
// Dry-run probes detect on in-range steps only — never apply, never the
|
|
189
|
-
// live runner.
|
|
190
|
-
assert.deepEqual(calls, ['detect:1.4.0', 'detect:1.5.0']);
|
|
191
|
-
assert.equal(runnerCalled, false);
|
|
192
|
-
assert.ok(!calls.some((c) => c.startsWith('apply:')));
|
|
193
|
-
|
|
194
|
-
// Operator-facing plan output.
|
|
195
|
-
const stdout = cap.out.join('');
|
|
196
|
-
assert.match(stdout, /dry run v1\.3\.0 → v1\.5\.0/);
|
|
197
|
-
assert.match(stdout, /would apply {2}1\.4\.0: rename foo to bar/);
|
|
198
|
-
assert.match(stdout, /would skip {3}1\.5\.0: move baseline file/);
|
|
199
|
-
assert.match(stdout, /no migrations applied, nothing written/);
|
|
200
|
-
assert.equal(cap.exitCode, null);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('reports an empty plan when no steps fall in range', () => {
|
|
204
|
-
const cap = makeCapture();
|
|
205
|
-
const { registry, calls } = makeFixtureRegistry();
|
|
206
|
-
|
|
207
|
-
const result = runMigrate({
|
|
208
|
-
// 1.6.0 < v <= 1.6.0 leaves only the 1.6.0 step out (exclusive lower);
|
|
209
|
-
// 9.9.0 → 9.9.0 range catches nothing.
|
|
210
|
-
argv: ['--from', '9.9.0', '--to', '9.9.0', '--dry-run'],
|
|
211
|
-
runMigrations: () => ({ applied: [], skipped: [] }),
|
|
212
|
-
registry,
|
|
213
|
-
write: cap.write,
|
|
214
|
-
writeErr: cap.writeErr,
|
|
215
|
-
exit: cap.exit,
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
assert.deepEqual(result.wouldApply, []);
|
|
219
|
-
assert.deepEqual(result.wouldSkip, []);
|
|
220
|
-
assert.deepEqual(calls, []);
|
|
221
|
-
assert.match(cap.out.join(''), /no migration steps in range/);
|
|
222
|
-
});
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
// AC — missing bounds are a usage error
|
|
227
|
-
// ---------------------------------------------------------------------------
|
|
228
|
-
|
|
229
|
-
describe('runMigrate — usage validation', () => {
|
|
230
|
-
it('exits non-zero when --to is missing', () => {
|
|
231
|
-
const cap = makeCapture();
|
|
232
|
-
let runnerCalled = false;
|
|
233
|
-
|
|
234
|
-
const result = runMigrate({
|
|
235
|
-
argv: ['--from', '1.3.0'],
|
|
236
|
-
runMigrations: () => {
|
|
237
|
-
runnerCalled = true;
|
|
238
|
-
return { applied: [], skipped: [] };
|
|
239
|
-
},
|
|
240
|
-
registry: [],
|
|
241
|
-
write: cap.write,
|
|
242
|
-
writeErr: cap.writeErr,
|
|
243
|
-
exit: cap.exit,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
assert.equal(result.ok, false);
|
|
247
|
-
assert.equal(result.action, 'usage-error');
|
|
248
|
-
assert.equal(cap.exitCode, 1);
|
|
249
|
-
assert.equal(runnerCalled, false);
|
|
250
|
-
assert.match(cap.err.join(''), /both --from .* and --to .* are required/);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('exits non-zero when --from is missing', () => {
|
|
254
|
-
const cap = makeCapture();
|
|
255
|
-
|
|
256
|
-
const result = runMigrate({
|
|
257
|
-
argv: ['--to', '1.5.0'],
|
|
258
|
-
runMigrations: () => ({ applied: [], skipped: [] }),
|
|
259
|
-
registry: [],
|
|
260
|
-
write: cap.write,
|
|
261
|
-
writeErr: cap.writeErr,
|
|
262
|
-
exit: cap.exit,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
assert.equal(result.action, 'usage-error');
|
|
266
|
-
assert.equal(cap.exitCode, 1);
|
|
267
|
-
});
|
|
268
|
-
});
|
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
// lib/cli/__tests__/sync-local-zone.test.js
|
|
2
|
-
/**
|
|
3
|
-
* Unit tests for the `.agents/local/` sync-exempt local-additions zone
|
|
4
|
-
* (Story #3498, f-drift-local-zone).
|
|
5
|
-
*
|
|
6
|
-
* Contract under test (Story #3498 AC):
|
|
7
|
-
* 1. runSync skips any path under `.agents/local/` when copying the
|
|
8
|
-
* package payload — proven by pre-populating a consumer-authored
|
|
9
|
-
* `.agents/local/custom.md` in the destination via an injected fs
|
|
10
|
-
* seam and asserting it is left byte-identical after a sync.
|
|
11
|
-
* 2. `mandrel sync` writes no file into `.agents/local/` from the package
|
|
12
|
-
* payload — proven by seeding a (hypothetical) payload file under the
|
|
13
|
-
* source `.agents/local/` and asserting it is never enumerated, never
|
|
14
|
-
* copied, and never appears in the dry-run plan.
|
|
15
|
-
*
|
|
16
|
-
* Every test drives runSync through the same injectable seams used by
|
|
17
|
-
* sync.test.js (resolvePackageRoot, fs, cwd, write, writeErr, exit) backed
|
|
18
|
-
* by an in-memory filesystem fake, so no real package resolution and no
|
|
19
|
-
* real disk I/O occur (testing-standards § Unit: all filesystem I/O MUST
|
|
20
|
-
* be mocked; sync.js injectable-seam style — no real child processes).
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import assert from 'node:assert/strict';
|
|
24
|
-
import path from 'node:path';
|
|
25
|
-
import { describe, it } from 'node:test';
|
|
26
|
-
|
|
27
|
-
import { runSync } from '../sync.js';
|
|
28
|
-
|
|
29
|
-
// ---------------------------------------------------------------------------
|
|
30
|
-
// In-memory filesystem fake (mirrors sync.test.js)
|
|
31
|
-
// ---------------------------------------------------------------------------
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Build an in-memory fs whose `seed` maps absolute file paths → contents.
|
|
35
|
-
* Directories are inferred from the seeded file paths.
|
|
36
|
-
*
|
|
37
|
-
* Tracks writes (copyFileSync) and guards against symlink creation.
|
|
38
|
-
*/
|
|
39
|
-
function makeFs(seed = {}) {
|
|
40
|
-
const files = new Map(Object.entries(seed));
|
|
41
|
-
const symlinkCalls = [];
|
|
42
|
-
|
|
43
|
-
function dirEntries(dir) {
|
|
44
|
-
const norm = dir.endsWith(path.sep) ? dir.slice(0, -1) : dir;
|
|
45
|
-
const children = new Map(); // name → isDir
|
|
46
|
-
for (const abs of files.keys()) {
|
|
47
|
-
if (!abs.startsWith(norm + path.sep)) continue;
|
|
48
|
-
const rest = abs.slice(norm.length + 1);
|
|
49
|
-
const segments = rest.split(path.sep);
|
|
50
|
-
const name = segments[0];
|
|
51
|
-
children.set(name, segments.length > 1);
|
|
52
|
-
}
|
|
53
|
-
return [...children.entries()].map(([name, isDir]) => ({
|
|
54
|
-
name,
|
|
55
|
-
isDirectory: () => isDir,
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
files,
|
|
61
|
-
symlinkCalls,
|
|
62
|
-
readdirSync(dir, _opts) {
|
|
63
|
-
return dirEntries(dir);
|
|
64
|
-
},
|
|
65
|
-
existsSync(p) {
|
|
66
|
-
const norm = p.endsWith(path.sep) ? p.slice(0, -1) : p;
|
|
67
|
-
if (files.has(norm)) return true;
|
|
68
|
-
for (const abs of files.keys()) {
|
|
69
|
-
if (abs.startsWith(norm + path.sep)) return true;
|
|
70
|
-
}
|
|
71
|
-
return false;
|
|
72
|
-
},
|
|
73
|
-
mkdirSync(_dir, _opts) {
|
|
74
|
-
// No-op: directories are implied by file paths in this fake.
|
|
75
|
-
},
|
|
76
|
-
copyFileSync(src, dest) {
|
|
77
|
-
if (!files.has(src)) {
|
|
78
|
-
throw new Error(`copyFileSync: source missing ${src}`);
|
|
79
|
-
}
|
|
80
|
-
files.set(dest, files.get(src));
|
|
81
|
-
},
|
|
82
|
-
symlinkSync(target, p) {
|
|
83
|
-
symlinkCalls.push({ target, path: p });
|
|
84
|
-
},
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/** Capture stdout/stderr writes and the exit code. */
|
|
89
|
-
function makeCapture() {
|
|
90
|
-
const out = [];
|
|
91
|
-
const err = [];
|
|
92
|
-
let exitCode = null;
|
|
93
|
-
return {
|
|
94
|
-
out,
|
|
95
|
-
err,
|
|
96
|
-
get exitCode() {
|
|
97
|
-
return exitCode;
|
|
98
|
-
},
|
|
99
|
-
write: (s) => out.push(s),
|
|
100
|
-
writeErr: (s) => err.push(s),
|
|
101
|
-
exit: (code) => {
|
|
102
|
-
exitCode = code;
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
const PROJECT = path.join(path.sep, 'proj');
|
|
108
|
-
const PKG_ROOT = path.join(PROJECT, 'node_modules', 'mandrel');
|
|
109
|
-
const SRC_AGENTS = path.join(PKG_ROOT, '.agents');
|
|
110
|
-
const resolveToPkg = () => PKG_ROOT;
|
|
111
|
-
|
|
112
|
-
/** Seed a normal package payload of two non-local files under <pkg>/.agents/. */
|
|
113
|
-
function seedPackagePayload() {
|
|
114
|
-
return {
|
|
115
|
-
[path.join(SRC_AGENTS, 'instructions.md')]: '# instructions\n',
|
|
116
|
-
[path.join(SRC_AGENTS, 'rules', 'security-baseline.md')]: '# security\n',
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const baseOpts = (fs, cap) => ({
|
|
121
|
-
argv: [],
|
|
122
|
-
resolvePackageRoot: resolveToPkg,
|
|
123
|
-
fs,
|
|
124
|
-
cwd: () => PROJECT,
|
|
125
|
-
write: cap.write,
|
|
126
|
-
writeErr: cap.writeErr,
|
|
127
|
-
exit: cap.exit,
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
// ---------------------------------------------------------------------------
|
|
131
|
-
// AC1 — pre-existing consumer .agents/local/ additions survive sync
|
|
132
|
-
// ---------------------------------------------------------------------------
|
|
133
|
-
|
|
134
|
-
describe('runSync — .agents/local/ consumer additions survive', () => {
|
|
135
|
-
it('leaves a pre-existing .agents/local/custom.md untouched', () => {
|
|
136
|
-
// Arrange: a normal payload plus a consumer-authored file already living
|
|
137
|
-
// in the destination's local zone.
|
|
138
|
-
const localAddition = path.join(PROJECT, '.agents', 'local', 'custom.md');
|
|
139
|
-
const fs = makeFs({
|
|
140
|
-
...seedPackagePayload(),
|
|
141
|
-
[localAddition]: '# my custom local note\n',
|
|
142
|
-
});
|
|
143
|
-
const cap = makeCapture();
|
|
144
|
-
|
|
145
|
-
// Act
|
|
146
|
-
const result = runSync(baseOpts(fs, cap));
|
|
147
|
-
|
|
148
|
-
// Assert: the local addition is byte-identical and the regular payload
|
|
149
|
-
// still materialized.
|
|
150
|
-
assert.equal(fs.files.get(localAddition), '# my custom local note\n');
|
|
151
|
-
assert.equal(
|
|
152
|
-
fs.files.get(path.join(PROJECT, '.agents', 'instructions.md')),
|
|
153
|
-
'# instructions\n',
|
|
154
|
-
);
|
|
155
|
-
assert.equal(result.copied, 2);
|
|
156
|
-
assert.equal(cap.exitCode, null);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('never writes into the destination .agents/local/ subtree', () => {
|
|
160
|
-
const localAddition = path.join(PROJECT, '.agents', 'local', 'custom.md');
|
|
161
|
-
const fs = makeFs({
|
|
162
|
-
...seedPackagePayload(),
|
|
163
|
-
[localAddition]: '# my custom local note\n',
|
|
164
|
-
});
|
|
165
|
-
const cap = makeCapture();
|
|
166
|
-
|
|
167
|
-
runSync(baseOpts(fs, cap));
|
|
168
|
-
|
|
169
|
-
// The only destination file under .agents/local/ is the one the consumer
|
|
170
|
-
// authored; sync added nothing there.
|
|
171
|
-
const localPrefix = path.join(PROJECT, '.agents', 'local') + path.sep;
|
|
172
|
-
const localDestFiles = [...fs.files.keys()].filter((k) =>
|
|
173
|
-
k.startsWith(localPrefix),
|
|
174
|
-
);
|
|
175
|
-
assert.deepEqual(localDestFiles, [localAddition]);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
// AC2 — a payload file under .agents/local/ is never materialized
|
|
181
|
-
// ---------------------------------------------------------------------------
|
|
182
|
-
|
|
183
|
-
describe('runSync — payload .agents/local/ is skipped on copy', () => {
|
|
184
|
-
it('does not copy a source .agents/local/ file into the destination', () => {
|
|
185
|
-
// Arrange: a payload that (hypothetically) ships a file under local/.
|
|
186
|
-
// The published payload ships none, but the skip must hold defensively.
|
|
187
|
-
const srcLocal = path.join(SRC_AGENTS, 'local', 'should-not-copy.md');
|
|
188
|
-
const fs = makeFs({
|
|
189
|
-
...seedPackagePayload(),
|
|
190
|
-
[srcLocal]: '# payload local file\n',
|
|
191
|
-
});
|
|
192
|
-
const cap = makeCapture();
|
|
193
|
-
|
|
194
|
-
// Act
|
|
195
|
-
const result = runSync(baseOpts(fs, cap));
|
|
196
|
-
|
|
197
|
-
// Assert: the local payload file was never copied to the destination.
|
|
198
|
-
const destLocal = path.join(
|
|
199
|
-
PROJECT,
|
|
200
|
-
'.agents',
|
|
201
|
-
'local',
|
|
202
|
-
'should-not-copy.md',
|
|
203
|
-
);
|
|
204
|
-
assert.equal(fs.files.has(destLocal), false);
|
|
205
|
-
// Only the two non-local payload files were materialized.
|
|
206
|
-
assert.equal(result.copied, 2);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('omits .agents/local/ paths from the --dry-run plan', () => {
|
|
210
|
-
const srcLocal = path.join(SRC_AGENTS, 'local', 'should-not-copy.md');
|
|
211
|
-
const fs = makeFs({
|
|
212
|
-
...seedPackagePayload(),
|
|
213
|
-
[srcLocal]: '# payload local file\n',
|
|
214
|
-
});
|
|
215
|
-
const cap = makeCapture();
|
|
216
|
-
|
|
217
|
-
const result = runSync({ ...baseOpts(fs, cap), argv: ['--dry-run'] });
|
|
218
|
-
|
|
219
|
-
const joined = cap.out.join('');
|
|
220
|
-
assert.doesNotMatch(joined, /local/);
|
|
221
|
-
assert.match(joined, /Dry run: 2 file\(s\)/);
|
|
222
|
-
assert.equal(result.planned, 2);
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('still materializes a deeper non-top-level directory named local', () => {
|
|
226
|
-
// The skip is scoped to the top-level .agents/local/ only — a nested
|
|
227
|
-
// rules/local/ must still copy.
|
|
228
|
-
const nestedLocal = path.join(SRC_AGENTS, 'rules', 'local', 'note.md');
|
|
229
|
-
const fs = makeFs({
|
|
230
|
-
...seedPackagePayload(),
|
|
231
|
-
[nestedLocal]: '# nested local note\n',
|
|
232
|
-
});
|
|
233
|
-
const cap = makeCapture();
|
|
234
|
-
|
|
235
|
-
const result = runSync(baseOpts(fs, cap));
|
|
236
|
-
|
|
237
|
-
const destNested = path.join(
|
|
238
|
-
PROJECT,
|
|
239
|
-
'.agents',
|
|
240
|
-
'rules',
|
|
241
|
-
'local',
|
|
242
|
-
'note.md',
|
|
243
|
-
);
|
|
244
|
-
assert.equal(fs.files.get(destNested), '# nested local note\n');
|
|
245
|
-
assert.equal(result.copied, 3);
|
|
246
|
-
});
|
|
247
|
-
});
|