@kontourai/flow-agents 1.3.0 → 2.0.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/.github/CODEOWNERS +29 -0
- package/.github/actions/trust-verify/action.yml +145 -0
- package/.github/workflows/ci.yml +11 -4
- package/.github/workflows/kit-gates-demo.yml +2 -2
- package/.github/workflows/publish-npm.yml +10 -2
- package/.github/workflows/release-please.yml +1 -1
- package/.github/workflows/trust-reconcile.yml +113 -0
- package/AGENTS.md +13 -0
- package/CHANGELOG.md +103 -0
- package/CONTRIBUTING.md +4 -4
- package/README.md +1 -0
- package/agents/tool-planner.json +1 -1
- package/build/src/cli/console-learning-projection.d.ts +1 -0
- package/build/src/cli/effective-backlog-settings.d.ts +1 -0
- package/build/src/cli/fixture-retirement-audit.d.ts +2 -0
- package/build/src/cli/init.d.ts +17 -0
- package/build/src/cli/init.js +242 -20
- package/build/src/cli/kit.d.ts +1 -0
- package/build/src/cli/promote-workflow-artifact.d.ts +1 -0
- package/build/src/cli/publish-change-helper.d.ts +1 -0
- package/build/src/cli/pull-work-provider.d.ts +1 -0
- package/build/src/cli/runtime-adapter.d.ts +1 -0
- package/build/src/cli/telemetry-doctor.d.ts +1 -0
- package/build/src/cli/usage-feedback.d.ts +1 -0
- package/build/src/cli/utterance-check.d.ts +1 -0
- package/build/src/cli/validate-hook-influence.d.ts +1 -0
- package/build/src/cli/validate-source-tree.d.ts +1 -0
- package/build/src/cli/validate-workflow-artifacts.d.ts +2 -0
- package/build/src/cli/validate-workflow-artifacts.js +19 -2
- package/build/src/cli/verify.d.ts +1 -0
- package/build/src/cli/verify.js +90 -0
- package/build/src/cli/veritas-governance.d.ts +1 -0
- package/build/src/cli/workflow-artifact-cleanup-audit.d.ts +1 -0
- package/build/src/cli/workflow-sidecar.d.ts +324 -0
- package/build/src/cli/workflow-sidecar.js +1973 -90
- package/build/src/cli.d.ts +2 -0
- package/build/src/cli.js +2 -3
- package/build/src/flow-kit/validate.d.ts +81 -0
- package/build/src/index.d.ts +5 -0
- package/build/src/index.js +36 -0
- package/build/src/lib/args.d.ts +8 -0
- package/build/src/lib/flow-resolver.d.ts +82 -0
- package/build/src/lib/flow-resolver.js +237 -0
- package/build/src/lib/fs.d.ts +7 -0
- package/build/src/lib/workflow-learning-projection.d.ts +132 -0
- package/build/src/runtime-adapters.d.ts +18 -0
- package/build/src/tools/build-universal-bundles.d.ts +2 -0
- package/build/src/tools/build-universal-bundles.js +34 -22
- package/build/src/tools/common.d.ts +9 -0
- package/build/src/tools/generate-context-map.d.ts +2 -0
- package/build/src/tools/generate-context-map.js +3 -16
- package/build/src/tools/validate-package.d.ts +2 -0
- package/build/src/tools/validate-source-tree.d.ts +2 -0
- package/build/src/tools/validate-source-tree.js +42 -162
- package/context/contracts/artifact-contract.md +10 -0
- package/context/contracts/delivery-contract.md +1 -0
- package/context/contracts/review-contract.md +1 -0
- package/context/contracts/verification-contract.md +2 -0
- package/context/gate-awareness.md +39 -0
- package/context/scripts/hooks/stop-goal-fit.js +632 -70
- package/docs/adr/0001-flow-agents-consumes-flow.md +1 -1
- package/docs/adr/0002-flow-kits-as-extension-unit.md +1 -1
- package/docs/adr/0004-gates-expect-surface-claims.md +2 -0
- package/docs/adr/0005-kubernetes-inspired-resource-contracts.md +2 -0
- package/docs/adr/0007-skill-audit.md +1 -1
- package/docs/adr/0009-canonical-hook-core-kit-boundary.md +95 -0
- package/docs/adr/0010-workflow-trust-state-as-hachure-bundle.md +139 -0
- package/docs/adr/0011-mcp-posture.md +100 -0
- package/docs/adr/0012-agent-coordination-as-liveness-claims.md +119 -0
- package/docs/adr/0013-context-lifecycle.md +151 -0
- package/docs/adr/0014-core-vs-domain-kit-boundary.md +143 -0
- package/docs/adr/0015-flow-flow-agents-boundary-reconciliation.md +120 -0
- package/docs/adr/0016-three-hard-boundary-model.md +71 -0
- package/docs/adr/0017-anti-gaming-trust-security-model.md +155 -0
- package/docs/agent-system-guidebook.md +5 -12
- package/docs/context-map.md +4 -10
- package/docs/developer-architecture.md +14 -0
- package/docs/index.md +3 -2
- package/docs/integrations/framework-adapter.md +19 -6
- package/docs/integrations/index.md +2 -2
- package/docs/north-star.md +4 -4
- package/docs/operating-layers.md +3 -3
- package/docs/plans/adr-0010-phase2-gate-recompute.md +55 -0
- package/docs/repository-structure.md +2 -2
- package/docs/skills-map.md +1 -0
- package/docs/spec/runtime-hook-surface.md +78 -10
- package/docs/standards-register.md +3 -3
- package/docs/survey-utterance-check.md +1 -1
- package/docs/trust-anchor-adoption.md +197 -0
- package/docs/verifiable-trust.md +95 -0
- package/docs/veritas-integration.md +2 -2
- package/docs/workflow-usage-guide.md +69 -0
- package/evals/acceptance/DEMO-false-completion.md +144 -0
- package/evals/acceptance/demo-cast.sh +92 -0
- package/evals/acceptance/demo-false-completion.sh +72 -0
- package/evals/acceptance/demo-real-evidence.sh +104 -0
- package/evals/acceptance/demo.tape +29 -0
- package/evals/acceptance/prove-capture-teeth-declared.sh +335 -0
- package/evals/acceptance/prove-capture-teeth.sh +114 -0
- package/evals/acceptance/prove-teeth.sh +105 -0
- package/evals/ci/antigaming-suite.sh +54 -0
- package/evals/ci/run-baseline.sh +2 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/invalid-missing-extension-asset/kit.json +20 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/flows/review.flow.json +26 -0
- package/evals/fixtures/flow-kit-repository/valid-unknown-extension/kit.json +18 -0
- package/evals/integration/test_builder_step_producers.sh +379 -0
- package/evals/integration/test_bundle_install.sh +35 -71
- package/evals/integration/test_bundle_lifecycle.sh +39 -2
- package/evals/integration/test_captured_fail_reconciliation.sh +820 -0
- package/evals/integration/test_checkpoint_signing.sh +489 -0
- package/evals/integration/test_claim_lookup.sh +352 -0
- package/evals/integration/test_command_log_integrity.sh +275 -0
- package/evals/integration/test_context_map.sh +0 -2
- package/evals/integration/test_dual_emit_flow_step.sh +278 -0
- package/evals/integration/test_enforcer_expects_driven.sh +281 -0
- package/evals/integration/test_evidence_capture_hook.sh +185 -0
- package/evals/integration/test_flow_kit_repository.sh +2 -0
- package/evals/integration/test_flowdef_session_activation.sh +273 -0
- package/evals/integration/test_flowdef_session_history_preservation.sh +250 -0
- package/evals/integration/test_gate_bypass_chain.sh +448 -0
- package/evals/integration/test_gate_lockdown.sh +1137 -0
- package/evals/integration/test_gate_review_inquiry_records.sh +399 -0
- package/evals/integration/test_goal_fit_escape_hatch.sh +73 -0
- package/evals/integration/test_goal_fit_hook.sh +69 -4
- package/evals/integration/test_goal_fit_rederive.sh +263 -0
- package/evals/integration/test_hook_category_behaviors.sh +14 -0
- package/evals/integration/test_install_merge.sh +1176 -0
- package/evals/integration/test_mint_attestation.sh +373 -0
- package/evals/integration/test_phase_map_and_gate_claim.sh +365 -0
- package/evals/integration/test_publish_delivery.sh +269 -0
- package/evals/integration/test_reconcile_soundness.sh +528 -0
- package/evals/integration/test_resolvefirststep_security.sh +208 -0
- package/evals/integration/test_session_resume_roundtrip.sh +286 -0
- package/evals/integration/test_trust_checkpoint.sh +325 -0
- package/evals/integration/test_trust_reconcile.sh +293 -0
- package/evals/integration/test_verify_cli.sh +208 -0
- package/evals/integration/test_workflow_sidecar_writer.sh +549 -34
- package/evals/lib/node.sh +0 -6
- package/evals/run.sh +47 -0
- package/evals/static/test_library_exports.sh +85 -0
- package/evals/static/test_universal_bundles.sh +15 -0
- package/evals/static/test_workflow_skills.sh +6 -13
- package/install.sh +0 -7
- package/integrations/strands-ts/README.md +25 -15
- package/integrations/veritas/flow-agents.adapter.json +1 -2
- package/kits/builder/flows/build.flow.json +59 -12
- package/kits/builder/kit.json +85 -15
- package/kits/builder/skills/continue-work/SKILL.md +116 -0
- package/kits/builder/skills/deliver/SKILL.md +36 -6
- package/kits/builder/skills/design-probe/SKILL.md +28 -0
- package/kits/builder/skills/execute-plan/SKILL.md +9 -1
- package/kits/builder/skills/gate-review/SKILL.md +234 -0
- package/kits/builder/skills/learning-review/SKILL.md +30 -0
- package/kits/builder/skills/pickup-probe/SKILL.md +29 -0
- package/kits/builder/skills/plan-work/SKILL.md +13 -1
- package/kits/builder/skills/pull-work/SKILL.md +19 -0
- package/kits/knowledge/adapters/default-store/index.js +38 -0
- package/kits/knowledge/adapters/flow-runner/index.js +1620 -0
- package/kits/knowledge/adapters/obsidian-store/index.js +36 -6
- package/kits/knowledge/docs/store-contract.md +314 -0
- package/kits/knowledge/evals/audit-freshness/suite.test.js +368 -0
- package/kits/knowledge/evals/canonicalize-category/suite.test.js +383 -0
- package/kits/knowledge/evals/contract-suite/suite.test.js +111 -0
- package/kits/knowledge/evals/detect-contradictions/suite.test.js +324 -0
- package/kits/knowledge/evals/entities/suite.test.js +40 -0
- package/kits/knowledge/evals/glossary-sync/suite.test.js +416 -0
- package/kits/knowledge/evals/hygiene-review/suite.test.js +396 -0
- package/kits/knowledge/evals/retirement/suite.test.js +145 -0
- package/kits/knowledge/flows/audit-freshness.flow.json +44 -0
- package/kits/knowledge/flows/canonicalize-category.flow.json +44 -0
- package/kits/knowledge/flows/detect-contradictions.flow.json +44 -0
- package/kits/knowledge/flows/glossary-sync.flow.json +61 -0
- package/kits/knowledge/flows/hygiene-review.flow.json +43 -0
- package/kits/knowledge/kit.json +51 -1
- package/package.json +13 -4
- package/packaging/conformance/README.md +10 -2
- package/packaging/conformance/fixtures/evidence-capture--allow-records-command.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-bundle-disputed-claim.json +29 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-capture-contradicts-claimed-pass.json +30 -0
- package/packaging/conformance/fixtures/stop-goal-fit--block-mode.json +23 -0
- package/packaging/conformance/fixtures/stop-goal-fit--off-mode.json +24 -0
- package/packaging/conformance/fixtures/stop-goal-fit--warn-active-delivery.json +5 -2
- package/packaging/conformance/fixtures/stop-goal-fit--warn-no-bundle.json +23 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-active-prompt.json +30 -0
- package/packaging/conformance/fixtures/workflow-steering--reground-session-start.json +30 -0
- package/packaging/conformance/run-conformance.js +1 -1
- package/scripts/README.md +2 -1
- package/scripts/build-universal-bundles.js +0 -1
- package/scripts/ci/mint-attestation.js +221 -0
- package/scripts/ci/trust-reconcile.js +545 -0
- package/scripts/hooks/config-protection.js +423 -1
- package/scripts/hooks/evidence-capture.js +348 -0
- package/scripts/hooks/lib/liveness-read.js +113 -0
- package/scripts/hooks/run-hook.js +6 -1
- package/scripts/hooks/stop-goal-fit.js +1471 -79
- package/scripts/hooks/workflow-steering.js +135 -5
- package/scripts/install-codex-home.sh +39 -0
- package/scripts/install-merge.js +330 -0
- package/src/cli/init.ts +218 -20
- package/src/cli/validate-workflow-artifacts.ts +18 -2
- package/src/cli/verify.ts +100 -0
- package/src/cli/workflow-sidecar.ts +2093 -84
- package/src/cli.ts +2 -3
- package/src/index.ts +53 -0
- package/src/lib/flow-resolver.ts +284 -0
- package/src/tools/build-universal-bundles.ts +34 -21
- package/src/tools/generate-context-map.ts +3 -17
- package/src/tools/validate-source-tree.ts +44 -104
- package/tsconfig.json +1 -0
- package/build/src/tools/filter-installed-packs.js +0 -135
- package/packaging/packs.json +0 -49
- package/scripts/filter-installed-packs.js +0 -2
- package/src/tools/filter-installed-packs.ts +0 -132
|
@@ -0,0 +1,1176 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_install_merge.sh — Install merge-aware tests for claude-code + codex
|
|
3
|
+
#
|
|
4
|
+
# Covers (claude-code):
|
|
5
|
+
# 1. Seeded-user-config: user keys + non-FA hook survive install (AC1).
|
|
6
|
+
# 2. Version-stamped first install: .flow-agents/install.json written (AC2).
|
|
7
|
+
# 3. Idempotent re-run: two consecutive installs produce identical settings.json (AC2).
|
|
8
|
+
# 4. In-place upgrade: FA hook block is replaced, user keys survive (AC2).
|
|
9
|
+
# 5. Global target: --global flag merges into FLOW_AGENTS_USER_CLAUDE_SETTINGS path (AC3).
|
|
10
|
+
# 6. Fresh-install with no prior settings.json: same result as original behavior (AC4).
|
|
11
|
+
#
|
|
12
|
+
# Covers (codex):
|
|
13
|
+
# C1. Seeded codex hooks.json: user non-FA hook survives install.
|
|
14
|
+
# C2. Version-stamped first install: .flow-agents/install.json written with runtime=codex.
|
|
15
|
+
# C3. Idempotent re-run: two consecutive installs produce identical hooks.json.
|
|
16
|
+
# C4. Manual proof: user Stop hook survives + FA added + idempotent.
|
|
17
|
+
#
|
|
18
|
+
# Runtime scope: claude-code + codex. opencode/pi/kiro deferred per plan.
|
|
19
|
+
# Self-cleaning: all temp dirs removed on exit.
|
|
20
|
+
#
|
|
21
|
+
set -euo pipefail
|
|
22
|
+
|
|
23
|
+
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
24
|
+
TMPDIR_EVAL="$(mktemp -d /tmp/install-merge.XXXXXX)"
|
|
25
|
+
pass=0
|
|
26
|
+
fail=0
|
|
27
|
+
|
|
28
|
+
cleanup() {
|
|
29
|
+
rm -rf "$TMPDIR_EVAL"
|
|
30
|
+
}
|
|
31
|
+
trap cleanup EXIT
|
|
32
|
+
|
|
33
|
+
_pass() { echo " ✓ $1"; pass=$((pass + 1)); }
|
|
34
|
+
_fail() { echo " ✗ $1"; fail=$((fail + 1)); }
|
|
35
|
+
|
|
36
|
+
echo "=== Install Merge-Aware Tests (claude-code) ==="
|
|
37
|
+
echo ""
|
|
38
|
+
|
|
39
|
+
# Ensure bundles are built
|
|
40
|
+
echo "--- Build ---"
|
|
41
|
+
if (cd "$ROOT_DIR" && npm run build:bundles >/dev/null 2>&1); then
|
|
42
|
+
_pass "bundle build completed"
|
|
43
|
+
else
|
|
44
|
+
_fail "bundle build failed"
|
|
45
|
+
echo "Results: 0/$((pass + fail + 1)) passed, $((fail + 1)) failed"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
echo ""
|
|
49
|
+
|
|
50
|
+
# ─── Scenario 1: Seeded user config ──────────────────────────────────────────
|
|
51
|
+
echo "--- Scenario 1: Seeded user config (user keys + non-FA hook survive) ---"
|
|
52
|
+
|
|
53
|
+
SEEDED_DEST="$TMPDIR_EVAL/seeded-claude"
|
|
54
|
+
mkdir -p "$SEEDED_DEST/.claude"
|
|
55
|
+
|
|
56
|
+
# Seed a settings.json with user keys AND a non-flow-agents hook
|
|
57
|
+
cat > "$SEEDED_DEST/.claude/settings.json" << 'JSON'
|
|
58
|
+
{
|
|
59
|
+
"permissions": {
|
|
60
|
+
"allow": ["Bash(usertool:*)"],
|
|
61
|
+
"customPermission": true
|
|
62
|
+
},
|
|
63
|
+
"myUserKey": "preserved-value",
|
|
64
|
+
"hooks": {
|
|
65
|
+
"Stop": [
|
|
66
|
+
{
|
|
67
|
+
"hooks": [
|
|
68
|
+
{
|
|
69
|
+
"type": "command",
|
|
70
|
+
"command": "echo user-stop-hook",
|
|
71
|
+
"timeout": 5
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
JSON
|
|
79
|
+
|
|
80
|
+
# Run install
|
|
81
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$SEEDED_DEST" >/dev/null 2>&1)
|
|
82
|
+
|
|
83
|
+
# Assert: FA hooks present
|
|
84
|
+
if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
|
|
85
|
+
const fs = require("node:fs");
|
|
86
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
87
|
+
const hooks = s.hooks || {};
|
|
88
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
89
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
90
|
+
);
|
|
91
|
+
if (!hasFA) throw new Error("Flow Agents telemetry hooks not found");
|
|
92
|
+
console.log("ok");
|
|
93
|
+
NODE
|
|
94
|
+
then
|
|
95
|
+
_pass "seeded: flow-agents hooks are present after install"
|
|
96
|
+
else
|
|
97
|
+
_fail "seeded: flow-agents hooks missing after install"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# Assert: user key 'myUserKey' survived
|
|
101
|
+
if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
|
|
102
|
+
const fs = require("node:fs");
|
|
103
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
104
|
+
if (s.myUserKey !== "preserved-value") throw new Error("myUserKey not preserved: " + JSON.stringify(s.myUserKey));
|
|
105
|
+
console.log("ok");
|
|
106
|
+
NODE
|
|
107
|
+
then
|
|
108
|
+
_pass "seeded: user key 'myUserKey' preserved"
|
|
109
|
+
else
|
|
110
|
+
_fail "seeded: user key 'myUserKey' was clobbered"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# Assert: user non-FA Stop hook survived
|
|
114
|
+
if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
|
|
115
|
+
const fs = require("node:fs");
|
|
116
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
117
|
+
const stopGroups = (s.hooks || {}).Stop || [];
|
|
118
|
+
const hasUserHook = stopGroups.some(
|
|
119
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-stop-hook"))
|
|
120
|
+
);
|
|
121
|
+
if (!hasUserHook) throw new Error("User Stop hook not found in: " + JSON.stringify(stopGroups));
|
|
122
|
+
console.log("ok");
|
|
123
|
+
NODE
|
|
124
|
+
then
|
|
125
|
+
_pass "seeded: non-FA user Stop hook survived"
|
|
126
|
+
else
|
|
127
|
+
_fail "seeded: non-FA user Stop hook was removed"
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# Assert: user's custom permissions are PRESERVED via deep-merge (#117 core promise).
|
|
131
|
+
# permissions deep-merges — flow-agents UNIONs its required allow/deny/ask entries
|
|
132
|
+
# and preserves the user's defaultMode + custom sub-keys; it never clobbers them.
|
|
133
|
+
if node - "$SEEDED_DEST/.claude/settings.json" << 'NODE'
|
|
134
|
+
const fs = require("node:fs");
|
|
135
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
136
|
+
const p = s.permissions || {};
|
|
137
|
+
if (p.customPermission !== true) throw new Error("user permissions.customPermission was clobbered: " + JSON.stringify(p));
|
|
138
|
+
if (!JSON.stringify(p.allow || []).includes("usertool")) throw new Error("user permissions.allow entry not preserved by union: " + JSON.stringify(p.allow));
|
|
139
|
+
console.log("ok");
|
|
140
|
+
NODE
|
|
141
|
+
then
|
|
142
|
+
_pass "seeded: user custom permissions preserved (deep-merge union, not clobbered)"
|
|
143
|
+
else
|
|
144
|
+
_fail "seeded: user custom permissions were clobbered"
|
|
145
|
+
fi
|
|
146
|
+
|
|
147
|
+
echo ""
|
|
148
|
+
|
|
149
|
+
# ─── Scenario 2: Version-stamped first install ───────────────────────────────
|
|
150
|
+
echo "--- Scenario 2: Version-stamped first install ---"
|
|
151
|
+
|
|
152
|
+
STAMP_DEST="$TMPDIR_EVAL/stamp-claude"
|
|
153
|
+
mkdir -p "$STAMP_DEST"
|
|
154
|
+
|
|
155
|
+
# Fresh install (no prior settings.json)
|
|
156
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$STAMP_DEST" >/dev/null 2>&1)
|
|
157
|
+
|
|
158
|
+
# Assert: .flow-agents/install.json exists with version and installedAt
|
|
159
|
+
if node - "$STAMP_DEST/.flow-agents/install.json" << 'NODE'
|
|
160
|
+
const fs = require("node:fs");
|
|
161
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
162
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
163
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
164
|
+
if (record.runtime !== "claude-code") throw new Error("install.json wrong runtime: " + record.runtime);
|
|
165
|
+
// Validate ISO 8601 format
|
|
166
|
+
const d = new Date(record.installedAt);
|
|
167
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
168
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
169
|
+
NODE
|
|
170
|
+
then
|
|
171
|
+
_pass "first-install: .flow-agents/install.json written with version+runtime+installedAt"
|
|
172
|
+
else
|
|
173
|
+
_fail "first-install: .flow-agents/install.json missing or invalid"
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
# Assert: fresh install produces valid settings.json with FA hooks
|
|
177
|
+
if node - "$STAMP_DEST/.claude/settings.json" << 'NODE'
|
|
178
|
+
const fs = require("node:fs");
|
|
179
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
180
|
+
const hooks = s.hooks || {};
|
|
181
|
+
const events = Object.keys(hooks);
|
|
182
|
+
if (events.length === 0) throw new Error("no hooks in settings.json");
|
|
183
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
184
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
185
|
+
);
|
|
186
|
+
if (!hasFA) throw new Error("FA telemetry hooks not found");
|
|
187
|
+
console.log("ok: events=" + events.join(","));
|
|
188
|
+
NODE
|
|
189
|
+
then
|
|
190
|
+
_pass "first-install: settings.json contains FA hooks (fresh-install path unchanged)"
|
|
191
|
+
else
|
|
192
|
+
_fail "first-install: settings.json missing or FA hooks absent"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
echo ""
|
|
196
|
+
|
|
197
|
+
# ─── Scenario 3: Idempotent re-run ───────────────────────────────────────────
|
|
198
|
+
echo "--- Scenario 3: Idempotent re-run ---"
|
|
199
|
+
|
|
200
|
+
IDEM_DEST="$TMPDIR_EVAL/idem-merge-claude"
|
|
201
|
+
mkdir -p "$IDEM_DEST"
|
|
202
|
+
|
|
203
|
+
# First install
|
|
204
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$IDEM_DEST" >/dev/null 2>&1)
|
|
205
|
+
|
|
206
|
+
# Capture hook count after first install
|
|
207
|
+
HOOKS_BEFORE=$(node -e "
|
|
208
|
+
const s = JSON.parse(require('fs').readFileSync('$IDEM_DEST/.claude/settings.json','utf8'));
|
|
209
|
+
const hooks = s.hooks || {};
|
|
210
|
+
let count = 0;
|
|
211
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
212
|
+
console.log(count);
|
|
213
|
+
" 2>/dev/null || echo "0")
|
|
214
|
+
|
|
215
|
+
# Second install (idempotent)
|
|
216
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$IDEM_DEST" >/dev/null 2>&1)
|
|
217
|
+
|
|
218
|
+
HOOKS_AFTER=$(node -e "
|
|
219
|
+
const s = JSON.parse(require('fs').readFileSync('$IDEM_DEST/.claude/settings.json','utf8'));
|
|
220
|
+
const hooks = s.hooks || {};
|
|
221
|
+
let count = 0;
|
|
222
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
223
|
+
console.log(count);
|
|
224
|
+
" 2>/dev/null || echo "0")
|
|
225
|
+
|
|
226
|
+
if [[ "$HOOKS_BEFORE" == "$HOOKS_AFTER" && -n "$HOOKS_BEFORE" && "$HOOKS_BEFORE" != "0" ]]; then
|
|
227
|
+
_pass "idempotent: re-install did not grow hooks array (before=$HOOKS_BEFORE after=$HOOKS_AFTER)"
|
|
228
|
+
else
|
|
229
|
+
_fail "idempotent: hook count changed (before=$HOOKS_BEFORE after=$HOOKS_AFTER)"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
echo ""
|
|
233
|
+
|
|
234
|
+
# ─── Scenario 4: User keys survive re-install ────────────────────────────────
|
|
235
|
+
echo "--- Scenario 4: User keys survive re-install (upgrade semantics) ---"
|
|
236
|
+
|
|
237
|
+
UPGRADE_DEST="$TMPDIR_EVAL/upgrade-claude"
|
|
238
|
+
mkdir -p "$UPGRADE_DEST/.claude"
|
|
239
|
+
|
|
240
|
+
# Seed with user key + non-FA hook
|
|
241
|
+
cat > "$UPGRADE_DEST/.claude/settings.json" << 'JSON'
|
|
242
|
+
{
|
|
243
|
+
"permissions": {"x": 1},
|
|
244
|
+
"hooks": {
|
|
245
|
+
"Stop": [
|
|
246
|
+
{
|
|
247
|
+
"hooks": [
|
|
248
|
+
{
|
|
249
|
+
"type": "command",
|
|
250
|
+
"command": "echo user-hook"
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
}
|
|
254
|
+
]
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
JSON
|
|
258
|
+
|
|
259
|
+
# First install
|
|
260
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$UPGRADE_DEST" >/dev/null 2>&1)
|
|
261
|
+
|
|
262
|
+
# Second install (upgrade / re-install)
|
|
263
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$UPGRADE_DEST" >/dev/null 2>&1)
|
|
264
|
+
|
|
265
|
+
# Assert: user Stop hook survived the second install
|
|
266
|
+
if node - "$UPGRADE_DEST/.claude/settings.json" << 'NODE'
|
|
267
|
+
const fs = require("node:fs");
|
|
268
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
269
|
+
const stopGroups = (s.hooks || {}).Stop || [];
|
|
270
|
+
const hasUser = stopGroups.some(
|
|
271
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-hook"))
|
|
272
|
+
);
|
|
273
|
+
if (!hasUser) throw new Error("User hook not found after re-install: " + JSON.stringify(stopGroups));
|
|
274
|
+
// Also assert FA hooks present (not stripped)
|
|
275
|
+
const hasFA = stopGroups.some(
|
|
276
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Running Flow Agents hook policy"))
|
|
277
|
+
);
|
|
278
|
+
if (!hasFA) throw new Error("FA policy hook missing from Stop after re-install");
|
|
279
|
+
console.log("ok");
|
|
280
|
+
NODE
|
|
281
|
+
then
|
|
282
|
+
_pass "upgrade: user Stop hook and FA policy hook both present after re-install"
|
|
283
|
+
else
|
|
284
|
+
_fail "upgrade: user Stop hook or FA policy hook missing after re-install"
|
|
285
|
+
fi
|
|
286
|
+
|
|
287
|
+
# Assert: FA hooks not duplicated
|
|
288
|
+
if node - "$UPGRADE_DEST/.claude/settings.json" << 'NODE'
|
|
289
|
+
const fs = require("node:fs");
|
|
290
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
291
|
+
const hooks = s.hooks || {};
|
|
292
|
+
let maxFA = 0;
|
|
293
|
+
for (const [event, groups] of Object.entries(hooks)) {
|
|
294
|
+
const faCount = groups.filter(
|
|
295
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
296
|
+
).length;
|
|
297
|
+
maxFA = Math.max(maxFA, faCount);
|
|
298
|
+
}
|
|
299
|
+
if (maxFA > 1) throw new Error("FA telemetry hooks duplicated (max " + maxFA + " per event)");
|
|
300
|
+
console.log("ok");
|
|
301
|
+
NODE
|
|
302
|
+
then
|
|
303
|
+
_pass "upgrade: FA hooks are not duplicated after re-install"
|
|
304
|
+
else
|
|
305
|
+
_fail "upgrade: FA hooks duplicated after re-install"
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
echo ""
|
|
309
|
+
|
|
310
|
+
# ─── Scenario 5: Global target ───────────────────────────────────────────────
|
|
311
|
+
echo "--- Scenario 5: Global target (--global flag merges into user settings) ---"
|
|
312
|
+
|
|
313
|
+
GLOBAL_SETTINGS_DIR="$TMPDIR_EVAL/global-settings"
|
|
314
|
+
mkdir -p "$GLOBAL_SETTINGS_DIR"
|
|
315
|
+
|
|
316
|
+
# Seed a "user-level" settings with a user key
|
|
317
|
+
cat > "$GLOBAL_SETTINGS_DIR/settings.json" << 'JSON'
|
|
318
|
+
{
|
|
319
|
+
"myGlobalKey": "global-preserved",
|
|
320
|
+
"hooks": {
|
|
321
|
+
"PreToolUse": [
|
|
322
|
+
{
|
|
323
|
+
"hooks": [
|
|
324
|
+
{
|
|
325
|
+
"type": "command",
|
|
326
|
+
"command": "echo global-user-hook"
|
|
327
|
+
}
|
|
328
|
+
]
|
|
329
|
+
}
|
|
330
|
+
]
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
JSON
|
|
334
|
+
|
|
335
|
+
# Run init --global, overriding the target via FLOW_AGENTS_USER_CLAUDE_SETTINGS.
|
|
336
|
+
# --global for claude-code: dest = dirname(FLOW_AGENTS_USER_CLAUDE_SETTINGS) = GLOBAL_SETTINGS_DIR.
|
|
337
|
+
# The global path writes settings.json directly at dest/settings.json (dest IS ~/.claude/).
|
|
338
|
+
FLOW_AGENTS_USER_CLAUDE_SETTINGS="$GLOBAL_SETTINGS_DIR/settings.json" \
|
|
339
|
+
node "$ROOT_DIR/build/src/cli.js" init --runtime claude-code --global --yes >/dev/null 2>&1 || true
|
|
340
|
+
|
|
341
|
+
# The settings.json was merged in-place at GLOBAL_SETTINGS_DIR/settings.json.
|
|
342
|
+
GLOBAL_SETTINGS_JSON="$GLOBAL_SETTINGS_DIR/settings.json"
|
|
343
|
+
|
|
344
|
+
if [[ -f "$GLOBAL_SETTINGS_JSON" ]] && node - "$GLOBAL_SETTINGS_JSON" << 'NODE'
|
|
345
|
+
const fs = require("node:fs");
|
|
346
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
347
|
+
if (s.myGlobalKey !== "global-preserved") throw new Error("myGlobalKey not preserved: " + JSON.stringify(s.myGlobalKey));
|
|
348
|
+
const hooks = s.hooks || {};
|
|
349
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
350
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
351
|
+
);
|
|
352
|
+
if (!hasFA) throw new Error("FA hooks not present in global settings after --global install");
|
|
353
|
+
const preuseGroups = (hooks.PreToolUse || []);
|
|
354
|
+
const hasUserHook = preuseGroups.some(
|
|
355
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo global-user-hook"))
|
|
356
|
+
);
|
|
357
|
+
if (!hasUserHook) throw new Error("global user hook not preserved in PreToolUse");
|
|
358
|
+
console.log("ok");
|
|
359
|
+
NODE
|
|
360
|
+
then
|
|
361
|
+
_pass "--global: user key + user hook survived; FA hooks present in global settings"
|
|
362
|
+
else
|
|
363
|
+
_fail "--global: merge into global settings failed or user key/hook lost"
|
|
364
|
+
fi
|
|
365
|
+
|
|
366
|
+
echo ""
|
|
367
|
+
|
|
368
|
+
# ─── Scenario 6: Manual proof (user-visible) ─────────────────────────────────
|
|
369
|
+
echo "--- Scenario 6: Manual proof — permissions + user hook survive, FA hooks added ---"
|
|
370
|
+
|
|
371
|
+
PROOF_DEST="$TMPDIR_EVAL/proof-claude"
|
|
372
|
+
mkdir -p "$PROOF_DEST/.claude"
|
|
373
|
+
|
|
374
|
+
cat > "$PROOF_DEST/.claude/settings.json" << 'JSON'
|
|
375
|
+
{
|
|
376
|
+
"permissions": {"x": 1},
|
|
377
|
+
"hooks": {
|
|
378
|
+
"Stop": [
|
|
379
|
+
{
|
|
380
|
+
"hooks": [
|
|
381
|
+
{
|
|
382
|
+
"type": "command",
|
|
383
|
+
"command": "echo user-hook"
|
|
384
|
+
}
|
|
385
|
+
]
|
|
386
|
+
}
|
|
387
|
+
]
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
JSON
|
|
391
|
+
|
|
392
|
+
echo "BEFORE install:"
|
|
393
|
+
node -e "
|
|
394
|
+
const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
|
|
395
|
+
console.log(' permissions:', JSON.stringify(s.permissions));
|
|
396
|
+
console.log(' Stop hook count:', (s.hooks?.Stop || []).length);
|
|
397
|
+
console.log(' User hook present:', (s.hooks?.Stop || []).some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-hook'))));
|
|
398
|
+
"
|
|
399
|
+
|
|
400
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$PROOF_DEST" >/dev/null 2>&1)
|
|
401
|
+
|
|
402
|
+
echo "AFTER first install:"
|
|
403
|
+
node -e "
|
|
404
|
+
const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
|
|
405
|
+
const stopGroups = s.hooks?.Stop || [];
|
|
406
|
+
console.log(' permissions:', JSON.stringify(s.permissions));
|
|
407
|
+
console.log(' Stop hook count:', stopGroups.length);
|
|
408
|
+
console.log(' User hook present:', stopGroups.some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-hook'))));
|
|
409
|
+
console.log(' FA goal-fit hook present:', stopGroups.some(g => (g.hooks||[]).some(h => String(h.statusMessage||'').includes('Running Flow Agents hook policy'))));
|
|
410
|
+
"
|
|
411
|
+
|
|
412
|
+
(cd "$ROOT_DIR/dist/claude-code" && bash install.sh "$PROOF_DEST" >/dev/null 2>&1)
|
|
413
|
+
|
|
414
|
+
echo "AFTER second install (idempotence check):"
|
|
415
|
+
HOOKS_AFTER_SECOND=$(node -e "
|
|
416
|
+
const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
|
|
417
|
+
const hooks = s.hooks || {};
|
|
418
|
+
let count = 0;
|
|
419
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
420
|
+
console.log(count);
|
|
421
|
+
" 2>/dev/null || echo "err")
|
|
422
|
+
echo " Total hook groups: $HOOKS_AFTER_SECOND"
|
|
423
|
+
|
|
424
|
+
HOOKS_AFTER_FIRST=$(node -e "
|
|
425
|
+
const s = JSON.parse(require('fs').readFileSync('$PROOF_DEST/.claude/settings.json','utf8'));
|
|
426
|
+
const hooks = s.hooks || {};
|
|
427
|
+
let count = 0;
|
|
428
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
429
|
+
console.log(count);
|
|
430
|
+
" 2>/dev/null || echo "err")
|
|
431
|
+
|
|
432
|
+
if [[ "$HOOKS_AFTER_FIRST" == "$HOOKS_AFTER_SECOND" ]]; then
|
|
433
|
+
_pass "manual proof: second install is idempotent (hook count stable at $HOOKS_AFTER_FIRST)"
|
|
434
|
+
else
|
|
435
|
+
_fail "manual proof: hook count changed from first to second install"
|
|
436
|
+
fi
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ─── Codex: Scenario C1: Seeded user hooks + non-FA hook survive ──────────────
|
|
440
|
+
echo "=== Install Merge-Aware Tests (codex) ==="
|
|
441
|
+
echo ""
|
|
442
|
+
echo "--- Codex Scenario C1: Seeded user hooks survive install ---"
|
|
443
|
+
|
|
444
|
+
CODEX_SEEDED="$TMPDIR_EVAL/codex-seeded"
|
|
445
|
+
mkdir -p "$CODEX_SEEDED/.codex"
|
|
446
|
+
|
|
447
|
+
# Seed a hooks.json with a user non-FA hook in Stop
|
|
448
|
+
cat > "$CODEX_SEEDED/.codex/hooks.json" << 'JSON'
|
|
449
|
+
{
|
|
450
|
+
"hooks": {
|
|
451
|
+
"Stop": [
|
|
452
|
+
{
|
|
453
|
+
"hooks": [
|
|
454
|
+
{
|
|
455
|
+
"type": "command",
|
|
456
|
+
"command": "echo user-codex-stop-hook",
|
|
457
|
+
"timeout": 5
|
|
458
|
+
}
|
|
459
|
+
]
|
|
460
|
+
}
|
|
461
|
+
]
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
JSON
|
|
465
|
+
|
|
466
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_SEEDED" >/dev/null 2>&1)
|
|
467
|
+
|
|
468
|
+
# Assert: FA telemetry hooks present
|
|
469
|
+
if node - "$CODEX_SEEDED/.codex/hooks.json" << 'NODE'
|
|
470
|
+
const fs = require("node:fs");
|
|
471
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
472
|
+
const hooks = s.hooks || {};
|
|
473
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
474
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
475
|
+
);
|
|
476
|
+
if (!hasFA) throw new Error("Flow Agents telemetry hooks not found in codex hooks.json");
|
|
477
|
+
console.log("ok");
|
|
478
|
+
NODE
|
|
479
|
+
then
|
|
480
|
+
_pass "codex seeded: FA telemetry hooks present after install"
|
|
481
|
+
else
|
|
482
|
+
_fail "codex seeded: FA telemetry hooks missing after install"
|
|
483
|
+
fi
|
|
484
|
+
|
|
485
|
+
# Assert: user non-FA Stop hook survived
|
|
486
|
+
if node - "$CODEX_SEEDED/.codex/hooks.json" << 'NODE'
|
|
487
|
+
const fs = require("node:fs");
|
|
488
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
489
|
+
const stopGroups = (s.hooks || {}).Stop || [];
|
|
490
|
+
const hasUserHook = stopGroups.some(
|
|
491
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-codex-stop-hook"))
|
|
492
|
+
);
|
|
493
|
+
if (!hasUserHook) throw new Error("User codex Stop hook not found: " + JSON.stringify(stopGroups));
|
|
494
|
+
console.log("ok");
|
|
495
|
+
NODE
|
|
496
|
+
then
|
|
497
|
+
_pass "codex seeded: non-FA user Stop hook survived install"
|
|
498
|
+
else
|
|
499
|
+
_fail "codex seeded: non-FA user Stop hook was removed"
|
|
500
|
+
fi
|
|
501
|
+
|
|
502
|
+
echo ""
|
|
503
|
+
|
|
504
|
+
# ─── Codex: Scenario C2: Version-stamped first install ───────────────────────
|
|
505
|
+
echo "--- Codex Scenario C2: Version-stamped first install ---"
|
|
506
|
+
|
|
507
|
+
CODEX_STAMP="$TMPDIR_EVAL/codex-stamp"
|
|
508
|
+
mkdir -p "$CODEX_STAMP"
|
|
509
|
+
|
|
510
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_STAMP" >/dev/null 2>&1)
|
|
511
|
+
|
|
512
|
+
# Assert: .flow-agents/install.json exists with runtime=codex
|
|
513
|
+
if node - "$CODEX_STAMP/.flow-agents/install.json" << 'NODE'
|
|
514
|
+
const fs = require("node:fs");
|
|
515
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
516
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
517
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
518
|
+
if (record.runtime !== "codex") throw new Error("install.json wrong runtime: " + record.runtime + " (expected codex)");
|
|
519
|
+
const d = new Date(record.installedAt);
|
|
520
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
521
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
522
|
+
NODE
|
|
523
|
+
then
|
|
524
|
+
_pass "codex first-install: .flow-agents/install.json written with version+runtime=codex+installedAt"
|
|
525
|
+
else
|
|
526
|
+
_fail "codex first-install: .flow-agents/install.json missing or invalid or wrong runtime"
|
|
527
|
+
fi
|
|
528
|
+
|
|
529
|
+
echo ""
|
|
530
|
+
|
|
531
|
+
# ─── Codex: Scenario C3: Idempotent re-run ───────────────────────────────────
|
|
532
|
+
echo "--- Codex Scenario C3: Idempotent re-run ---"
|
|
533
|
+
|
|
534
|
+
CODEX_IDEM="$TMPDIR_EVAL/codex-idem"
|
|
535
|
+
mkdir -p "$CODEX_IDEM"
|
|
536
|
+
|
|
537
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_IDEM" >/dev/null 2>&1)
|
|
538
|
+
|
|
539
|
+
CODEX_HOOKS_BEFORE=$(node -e "
|
|
540
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_IDEM/.codex/hooks.json','utf8'));
|
|
541
|
+
const hooks = s.hooks || {};
|
|
542
|
+
let count = 0;
|
|
543
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
544
|
+
console.log(count);
|
|
545
|
+
" 2>/dev/null || echo "0")
|
|
546
|
+
|
|
547
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_IDEM" >/dev/null 2>&1)
|
|
548
|
+
|
|
549
|
+
CODEX_HOOKS_AFTER=$(node -e "
|
|
550
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_IDEM/.codex/hooks.json','utf8'));
|
|
551
|
+
const hooks = s.hooks || {};
|
|
552
|
+
let count = 0;
|
|
553
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
554
|
+
console.log(count);
|
|
555
|
+
" 2>/dev/null || echo "0")
|
|
556
|
+
|
|
557
|
+
if [[ "$CODEX_HOOKS_BEFORE" == "$CODEX_HOOKS_AFTER" && -n "$CODEX_HOOKS_BEFORE" && "$CODEX_HOOKS_BEFORE" != "0" ]]; then
|
|
558
|
+
_pass "codex idempotent: re-install did not grow hooks (before=$CODEX_HOOKS_BEFORE after=$CODEX_HOOKS_AFTER)"
|
|
559
|
+
else
|
|
560
|
+
_fail "codex idempotent: hook count changed (before=$CODEX_HOOKS_BEFORE after=$CODEX_HOOKS_AFTER)"
|
|
561
|
+
fi
|
|
562
|
+
|
|
563
|
+
echo ""
|
|
564
|
+
|
|
565
|
+
# ─── Codex: Scenario C4: Manual proof ───────────────────────────────────────
|
|
566
|
+
echo "--- Codex Scenario C4: Manual proof — user Stop hook survives, FA added, idempotent ---"
|
|
567
|
+
|
|
568
|
+
CODEX_PROOF="$TMPDIR_EVAL/codex-proof"
|
|
569
|
+
mkdir -p "$CODEX_PROOF/.codex"
|
|
570
|
+
|
|
571
|
+
cat > "$CODEX_PROOF/.codex/hooks.json" << 'JSON'
|
|
572
|
+
{
|
|
573
|
+
"hooks": {
|
|
574
|
+
"Stop": [
|
|
575
|
+
{
|
|
576
|
+
"hooks": [
|
|
577
|
+
{
|
|
578
|
+
"type": "command",
|
|
579
|
+
"command": "echo user-codex-hook",
|
|
580
|
+
"timeout": 5
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
}
|
|
584
|
+
]
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
JSON
|
|
588
|
+
|
|
589
|
+
echo "BEFORE codex install:"
|
|
590
|
+
node -e "
|
|
591
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
|
|
592
|
+
console.log(' Stop hook count:', (s.hooks?.Stop || []).length);
|
|
593
|
+
console.log(' User hook present:', (s.hooks?.Stop || []).some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-codex-hook'))));
|
|
594
|
+
"
|
|
595
|
+
|
|
596
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_PROOF" >/dev/null 2>&1)
|
|
597
|
+
|
|
598
|
+
echo "AFTER first codex install:"
|
|
599
|
+
node -e "
|
|
600
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
|
|
601
|
+
const stopGroups = s.hooks?.Stop || [];
|
|
602
|
+
console.log(' Stop hook count:', stopGroups.length);
|
|
603
|
+
console.log(' User hook present:', stopGroups.some(g => (g.hooks||[]).some(h => h.command?.includes('echo user-codex-hook'))));
|
|
604
|
+
console.log(' FA goal-fit hook present:', stopGroups.some(g => (g.hooks||[]).some(h => String(h.statusMessage||'').includes('Running Flow Agents hook policy'))));
|
|
605
|
+
"
|
|
606
|
+
|
|
607
|
+
(cd "$ROOT_DIR/dist/codex" && bash install.sh "$CODEX_PROOF" >/dev/null 2>&1)
|
|
608
|
+
|
|
609
|
+
echo "AFTER second codex install (idempotence check):"
|
|
610
|
+
CODEX_PROOF_HOOKS=$(node -e "
|
|
611
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
|
|
612
|
+
const hooks = s.hooks || {};
|
|
613
|
+
let count = 0;
|
|
614
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
615
|
+
console.log(count);
|
|
616
|
+
" 2>/dev/null || echo "err")
|
|
617
|
+
echo " Total hook groups: $CODEX_PROOF_HOOKS"
|
|
618
|
+
|
|
619
|
+
CODEX_PROOF_FIRST=$(node -e "
|
|
620
|
+
const s = JSON.parse(require('fs').readFileSync('$CODEX_PROOF/.codex/hooks.json','utf8'));
|
|
621
|
+
const hooks = s.hooks || {};
|
|
622
|
+
let count = 0;
|
|
623
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
624
|
+
console.log(count);
|
|
625
|
+
" 2>/dev/null || echo "err")
|
|
626
|
+
|
|
627
|
+
if [[ "$CODEX_PROOF_FIRST" == "$CODEX_PROOF_HOOKS" ]]; then
|
|
628
|
+
_pass "codex manual proof: second install is idempotent (hook count stable at $CODEX_PROOF_FIRST)"
|
|
629
|
+
else
|
|
630
|
+
_fail "codex manual proof: hook count changed from first to second install"
|
|
631
|
+
fi
|
|
632
|
+
|
|
633
|
+
# Assert user hook survived second install
|
|
634
|
+
if node - "$CODEX_PROOF/.codex/hooks.json" << 'NODE'
|
|
635
|
+
const fs = require("node:fs");
|
|
636
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
637
|
+
const stopGroups = (s.hooks || {}).Stop || [];
|
|
638
|
+
const hasUser = stopGroups.some(
|
|
639
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo user-codex-hook"))
|
|
640
|
+
);
|
|
641
|
+
if (!hasUser) throw new Error("User codex hook not found after second install");
|
|
642
|
+
const hasFA = stopGroups.some(
|
|
643
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Running Flow Agents hook policy"))
|
|
644
|
+
);
|
|
645
|
+
if (!hasFA) throw new Error("FA goal-fit hook missing from codex Stop after second install");
|
|
646
|
+
console.log("ok");
|
|
647
|
+
NODE
|
|
648
|
+
then
|
|
649
|
+
_pass "codex manual proof: user Stop hook + FA hooks both present after second install"
|
|
650
|
+
else
|
|
651
|
+
_fail "codex manual proof: user Stop hook or FA hooks missing after second install"
|
|
652
|
+
fi
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
# ─── opencode: Scenario OC1: User keys survive + $schema present + no empty hooks ─
|
|
657
|
+
echo "=== Install Merge-Aware Tests (opencode) ==="
|
|
658
|
+
echo ""
|
|
659
|
+
echo "--- opencode Scenario OC1: User keys survive + \$schema present + no spurious empty hooks ---"
|
|
660
|
+
|
|
661
|
+
OPENCODE_SEEDED="$TMPDIR_EVAL/opencode-seeded"
|
|
662
|
+
mkdir -p "$OPENCODE_SEEDED"
|
|
663
|
+
|
|
664
|
+
# Seed opencode.json with user keys (model + plugin)
|
|
665
|
+
cat > "$OPENCODE_SEEDED/opencode.json" << 'JSON'
|
|
666
|
+
{
|
|
667
|
+
"model": "x",
|
|
668
|
+
"plugin": ["user-thing"]
|
|
669
|
+
}
|
|
670
|
+
JSON
|
|
671
|
+
|
|
672
|
+
(cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_SEEDED" >/dev/null 2>&1)
|
|
673
|
+
|
|
674
|
+
# Assert: user 'model' key survived
|
|
675
|
+
if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
|
|
676
|
+
const fs = require("node:fs");
|
|
677
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
678
|
+
if (s.model !== "x") throw new Error("user key 'model' was clobbered: " + JSON.stringify(s));
|
|
679
|
+
console.log("ok");
|
|
680
|
+
NODE
|
|
681
|
+
then
|
|
682
|
+
_pass "opencode seeded: user key 'model' survived install"
|
|
683
|
+
else
|
|
684
|
+
_fail "opencode seeded: user key 'model' was clobbered"
|
|
685
|
+
fi
|
|
686
|
+
|
|
687
|
+
# Assert: $schema present
|
|
688
|
+
if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
|
|
689
|
+
const fs = require("node:fs");
|
|
690
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
691
|
+
if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing or wrong: " + JSON.stringify(s));
|
|
692
|
+
console.log("ok");
|
|
693
|
+
NODE
|
|
694
|
+
then
|
|
695
|
+
_pass "opencode seeded: \$schema present after install"
|
|
696
|
+
else
|
|
697
|
+
_fail "opencode seeded: \$schema missing after install"
|
|
698
|
+
fi
|
|
699
|
+
|
|
700
|
+
# Assert: user 'plugin' array survived
|
|
701
|
+
if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
|
|
702
|
+
const fs = require("node:fs");
|
|
703
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
704
|
+
if (!Array.isArray(s.plugin) || s.plugin[0] !== "user-thing") throw new Error("user key 'plugin' was clobbered: " + JSON.stringify(s));
|
|
705
|
+
console.log("ok");
|
|
706
|
+
NODE
|
|
707
|
+
then
|
|
708
|
+
_pass "opencode seeded: user key 'plugin' survived install"
|
|
709
|
+
else
|
|
710
|
+
_fail "opencode seeded: user key 'plugin' was clobbered"
|
|
711
|
+
fi
|
|
712
|
+
|
|
713
|
+
# Assert: no spurious empty hooks key
|
|
714
|
+
if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
|
|
715
|
+
const fs = require("node:fs");
|
|
716
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
717
|
+
if ("hooks" in s) throw new Error("spurious empty 'hooks' key found: " + JSON.stringify(s));
|
|
718
|
+
console.log("ok");
|
|
719
|
+
NODE
|
|
720
|
+
then
|
|
721
|
+
_pass "opencode seeded: no spurious empty 'hooks' key injected"
|
|
722
|
+
else
|
|
723
|
+
_fail "opencode seeded: spurious empty 'hooks' key was injected"
|
|
724
|
+
fi
|
|
725
|
+
|
|
726
|
+
# Assert: idempotent (install again, same result)
|
|
727
|
+
(cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_SEEDED" >/dev/null 2>&1)
|
|
728
|
+
if node - "$OPENCODE_SEEDED/opencode.json" << 'NODE'
|
|
729
|
+
const fs = require("node:fs");
|
|
730
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
731
|
+
if (s.model !== "x") throw new Error("user key clobbered on re-install");
|
|
732
|
+
if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing after re-install");
|
|
733
|
+
if ("hooks" in s) throw new Error("spurious hooks key on re-install: " + JSON.stringify(s));
|
|
734
|
+
console.log("ok");
|
|
735
|
+
NODE
|
|
736
|
+
then
|
|
737
|
+
_pass "opencode seeded: second install is idempotent (user keys + \$schema stable, no hooks)"
|
|
738
|
+
else
|
|
739
|
+
_fail "opencode seeded: second install changed the result"
|
|
740
|
+
fi
|
|
741
|
+
|
|
742
|
+
echo ""
|
|
743
|
+
|
|
744
|
+
# ─── opencode: Scenario OC2: Manual proof ───────────────────────────────────────
|
|
745
|
+
echo "--- opencode Scenario OC2: Manual proof — seed opencode.json with user keys ---"
|
|
746
|
+
|
|
747
|
+
OPENCODE_PROOF="$TMPDIR_EVAL/opencode-proof"
|
|
748
|
+
mkdir -p "$OPENCODE_PROOF"
|
|
749
|
+
|
|
750
|
+
cat > "$OPENCODE_PROOF/opencode.json" << 'JSON'
|
|
751
|
+
{"model":"x","plugin":["user-thing"]}
|
|
752
|
+
JSON
|
|
753
|
+
|
|
754
|
+
echo "BEFORE opencode install:"
|
|
755
|
+
node -e "const s=JSON.parse(require('fs').readFileSync('$OPENCODE_PROOF/opencode.json','utf8')); console.log(' opencode.json:', JSON.stringify(s));"
|
|
756
|
+
|
|
757
|
+
(cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OPENCODE_PROOF" >/dev/null 2>&1)
|
|
758
|
+
|
|
759
|
+
echo "AFTER opencode install:"
|
|
760
|
+
node -e "
|
|
761
|
+
const s=JSON.parse(require('fs').readFileSync('$OPENCODE_PROOF/opencode.json','utf8'));
|
|
762
|
+
console.log(' opencode.json:', JSON.stringify(s));
|
|
763
|
+
console.log(' user key model:', s.model);
|
|
764
|
+
console.log(' \$schema:', s['\$schema']);
|
|
765
|
+
console.log(' has hooks key:', 'hooks' in s);
|
|
766
|
+
"
|
|
767
|
+
|
|
768
|
+
echo ""
|
|
769
|
+
|
|
770
|
+
# ─── Version Stamp Tests (opencode, pi, kiro) ─────────────────────────────────
|
|
771
|
+
echo "=== Version Stamp Tests (opencode / pi / kiro / base) ==="
|
|
772
|
+
echo ""
|
|
773
|
+
|
|
774
|
+
echo "--- VS1: opencode install writes .flow-agents/install.json with runtime=opencode ---"
|
|
775
|
+
|
|
776
|
+
OC_STAMP="$TMPDIR_EVAL/opencode-stamp"
|
|
777
|
+
mkdir -p "$OC_STAMP"
|
|
778
|
+
|
|
779
|
+
(cd "$ROOT_DIR/dist/opencode" && bash install.sh "$OC_STAMP" >/dev/null 2>&1)
|
|
780
|
+
|
|
781
|
+
if node - "$OC_STAMP/.flow-agents/install.json" << 'NODE'
|
|
782
|
+
const fs = require("node:fs");
|
|
783
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
784
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
785
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
786
|
+
if (record.runtime !== "opencode") throw new Error("wrong runtime: " + record.runtime + " (expected opencode)");
|
|
787
|
+
const d = new Date(record.installedAt);
|
|
788
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
789
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
790
|
+
NODE
|
|
791
|
+
then
|
|
792
|
+
_pass "opencode install: .flow-agents/install.json written with runtime=opencode"
|
|
793
|
+
else
|
|
794
|
+
_fail "opencode install: .flow-agents/install.json missing or wrong runtime"
|
|
795
|
+
fi
|
|
796
|
+
|
|
797
|
+
echo ""
|
|
798
|
+
echo "--- VS2: pi install writes .flow-agents/install.json with runtime=pi ---"
|
|
799
|
+
|
|
800
|
+
PI_STAMP="$TMPDIR_EVAL/pi-stamp"
|
|
801
|
+
mkdir -p "$PI_STAMP"
|
|
802
|
+
|
|
803
|
+
(cd "$ROOT_DIR/dist/pi" && bash install.sh "$PI_STAMP" >/dev/null 2>&1)
|
|
804
|
+
|
|
805
|
+
if node - "$PI_STAMP/.flow-agents/install.json" << 'NODE'
|
|
806
|
+
const fs = require("node:fs");
|
|
807
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
808
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
809
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
810
|
+
if (record.runtime !== "pi") throw new Error("wrong runtime: " + record.runtime + " (expected pi)");
|
|
811
|
+
const d = new Date(record.installedAt);
|
|
812
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
813
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
814
|
+
NODE
|
|
815
|
+
then
|
|
816
|
+
_pass "pi install: .flow-agents/install.json written with runtime=pi"
|
|
817
|
+
else
|
|
818
|
+
_fail "pi install: .flow-agents/install.json missing or wrong runtime"
|
|
819
|
+
fi
|
|
820
|
+
|
|
821
|
+
echo ""
|
|
822
|
+
echo "--- VS3: kiro install writes .flow-agents/install.json with runtime=kiro ---"
|
|
823
|
+
|
|
824
|
+
KIRO_STAMP="$TMPDIR_EVAL/kiro-stamp"
|
|
825
|
+
mkdir -p "$KIRO_STAMP"
|
|
826
|
+
|
|
827
|
+
(cd "$ROOT_DIR/dist/kiro" && bash install.sh "$KIRO_STAMP" >/dev/null 2>&1)
|
|
828
|
+
|
|
829
|
+
if node - "$KIRO_STAMP/.flow-agents/install.json" << 'NODE'
|
|
830
|
+
const fs = require("node:fs");
|
|
831
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
832
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
833
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
834
|
+
if (record.runtime !== "kiro") throw new Error("wrong runtime: " + record.runtime + " (expected kiro)");
|
|
835
|
+
const d = new Date(record.installedAt);
|
|
836
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
837
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
838
|
+
NODE
|
|
839
|
+
then
|
|
840
|
+
_pass "kiro install: .flow-agents/install.json written with runtime=kiro"
|
|
841
|
+
else
|
|
842
|
+
_fail "kiro install: .flow-agents/install.json missing or wrong runtime"
|
|
843
|
+
fi
|
|
844
|
+
|
|
845
|
+
echo ""
|
|
846
|
+
echo "--- VS4: base install writes .flow-agents/install.json with runtime=base ---"
|
|
847
|
+
|
|
848
|
+
BASE_STAMP="$TMPDIR_EVAL/base-stamp"
|
|
849
|
+
mkdir -p "$BASE_STAMP"
|
|
850
|
+
|
|
851
|
+
(cd "$ROOT_DIR/dist/base" && bash install.sh "$BASE_STAMP" >/dev/null 2>&1)
|
|
852
|
+
|
|
853
|
+
if node - "$BASE_STAMP/.flow-agents/install.json" << 'NODE'
|
|
854
|
+
const fs = require("node:fs");
|
|
855
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
856
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
857
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
858
|
+
if (record.runtime !== "base") throw new Error("wrong runtime: " + record.runtime + " (expected base)");
|
|
859
|
+
const d = new Date(record.installedAt);
|
|
860
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
861
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
862
|
+
NODE
|
|
863
|
+
then
|
|
864
|
+
_pass "base install: .flow-agents/install.json written with runtime=base"
|
|
865
|
+
else
|
|
866
|
+
_fail "base install: .flow-agents/install.json missing or wrong runtime"
|
|
867
|
+
fi
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
|
|
871
|
+
# ─── codex-home: CH1: merge — user Stop hook survives install-codex-home ─────
|
|
872
|
+
echo "=== Install Merge-Aware Tests (codex-home) ==="
|
|
873
|
+
echo ""
|
|
874
|
+
echo "--- CH1: codex-home merge — seed user Stop hook → install → user hook survives + FA hooks present ---"
|
|
875
|
+
|
|
876
|
+
CH1_DEST="$TMPDIR_EVAL/codex-home-ch1"
|
|
877
|
+
mkdir -p "$CH1_DEST"
|
|
878
|
+
|
|
879
|
+
# Seed a user Stop hook in the codex-home hooks.json (at root, where it lives after flatten)
|
|
880
|
+
cat > "$CH1_DEST/hooks.json" << 'JSON'
|
|
881
|
+
{
|
|
882
|
+
"hooks": {
|
|
883
|
+
"Stop": [
|
|
884
|
+
{
|
|
885
|
+
"hooks": [
|
|
886
|
+
{
|
|
887
|
+
"type": "command",
|
|
888
|
+
"command": "echo ch1-user-stop-hook",
|
|
889
|
+
"timeout": 5
|
|
890
|
+
}
|
|
891
|
+
]
|
|
892
|
+
}
|
|
893
|
+
]
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
JSON
|
|
897
|
+
|
|
898
|
+
# Run install-codex-home.sh pointing to the isolated dest
|
|
899
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH1_DEST" >/dev/null 2>&1
|
|
900
|
+
|
|
901
|
+
# Assert: FA telemetry hooks present in $DEST/hooks.json
|
|
902
|
+
if node - "$CH1_DEST/hooks.json" << 'NODE'
|
|
903
|
+
const fs = require("node:fs");
|
|
904
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
905
|
+
const hooks = s.hooks || {};
|
|
906
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
907
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
908
|
+
);
|
|
909
|
+
if (!hasFA) throw new Error("FA telemetry hooks not found in codex-home hooks.json");
|
|
910
|
+
console.log("ok");
|
|
911
|
+
NODE
|
|
912
|
+
then
|
|
913
|
+
_pass "CH1: FA telemetry hooks present after install-codex-home"
|
|
914
|
+
else
|
|
915
|
+
_fail "CH1: FA telemetry hooks missing after install-codex-home"
|
|
916
|
+
fi
|
|
917
|
+
|
|
918
|
+
# Assert: user non-FA Stop hook survived
|
|
919
|
+
if node - "$CH1_DEST/hooks.json" << 'NODE'
|
|
920
|
+
const fs = require("node:fs");
|
|
921
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
922
|
+
const stopGroups = (s.hooks || {}).Stop || [];
|
|
923
|
+
const hasUser = stopGroups.some(
|
|
924
|
+
(g) => (g.hooks || []).some((h) => String(h.command || "").includes("echo ch1-user-stop-hook"))
|
|
925
|
+
);
|
|
926
|
+
if (!hasUser) throw new Error("User Stop hook not found in codex-home after install: " + JSON.stringify(stopGroups));
|
|
927
|
+
console.log("ok");
|
|
928
|
+
NODE
|
|
929
|
+
then
|
|
930
|
+
_pass "CH1: user Stop hook survived install-codex-home (merge, not overwrite)"
|
|
931
|
+
else
|
|
932
|
+
_fail "CH1: user Stop hook was overwritten by install-codex-home"
|
|
933
|
+
fi
|
|
934
|
+
|
|
935
|
+
echo ""
|
|
936
|
+
|
|
937
|
+
# ─── codex-home: CH2: stamp — install.json runtime=codex + version + installedAt ─
|
|
938
|
+
echo "--- CH2: codex-home stamp — install.json runtime=codex + version + installedAt ---"
|
|
939
|
+
|
|
940
|
+
CH2_DEST="$TMPDIR_EVAL/codex-home-ch2"
|
|
941
|
+
mkdir -p "$CH2_DEST"
|
|
942
|
+
|
|
943
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH2_DEST" >/dev/null 2>&1
|
|
944
|
+
|
|
945
|
+
if node - "$CH2_DEST/.flow-agents/install.json" << 'NODE'
|
|
946
|
+
const fs = require("node:fs");
|
|
947
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
948
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
949
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
950
|
+
if (record.runtime !== "codex") throw new Error("wrong runtime: " + record.runtime + " (expected codex)");
|
|
951
|
+
const d = new Date(record.installedAt);
|
|
952
|
+
if (isNaN(d.getTime())) throw new Error("installedAt not valid ISO date: " + record.installedAt);
|
|
953
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
954
|
+
NODE
|
|
955
|
+
then
|
|
956
|
+
_pass "CH2: codex-home install.json written with runtime=codex + version + installedAt"
|
|
957
|
+
else
|
|
958
|
+
_fail "CH2: codex-home install.json missing or wrong"
|
|
959
|
+
fi
|
|
960
|
+
|
|
961
|
+
echo ""
|
|
962
|
+
|
|
963
|
+
# ─── codex-home: CH3: idempotent — stable hook count on re-run ───────────────
|
|
964
|
+
echo "--- CH3: codex-home idempotent — stable hook count on re-run ---"
|
|
965
|
+
|
|
966
|
+
CH3_DEST="$TMPDIR_EVAL/codex-home-ch3"
|
|
967
|
+
mkdir -p "$CH3_DEST"
|
|
968
|
+
|
|
969
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH3_DEST" >/dev/null 2>&1
|
|
970
|
+
|
|
971
|
+
CH3_BEFORE=$(node -e "
|
|
972
|
+
const s = JSON.parse(require('fs').readFileSync('$CH3_DEST/hooks.json','utf8'));
|
|
973
|
+
const hooks = s.hooks || {};
|
|
974
|
+
let count = 0;
|
|
975
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
976
|
+
console.log(count);
|
|
977
|
+
" 2>/dev/null || echo "0")
|
|
978
|
+
|
|
979
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" bash "$ROOT_DIR/scripts/install-codex-home.sh" "$CH3_DEST" >/dev/null 2>&1
|
|
980
|
+
|
|
981
|
+
CH3_AFTER=$(node -e "
|
|
982
|
+
const s = JSON.parse(require('fs').readFileSync('$CH3_DEST/hooks.json','utf8'));
|
|
983
|
+
const hooks = s.hooks || {};
|
|
984
|
+
let count = 0;
|
|
985
|
+
for (const k of Object.keys(hooks)) count += (hooks[k] || []).length;
|
|
986
|
+
console.log(count);
|
|
987
|
+
" 2>/dev/null || echo "0")
|
|
988
|
+
|
|
989
|
+
if [[ "$CH3_BEFORE" == "$CH3_AFTER" && -n "$CH3_BEFORE" && "$CH3_BEFORE" != "0" ]]; then
|
|
990
|
+
_pass "CH3: codex-home hook count stable on re-run (before=$CH3_BEFORE after=$CH3_AFTER)"
|
|
991
|
+
else
|
|
992
|
+
_fail "CH3: codex-home hook count changed on re-run (before=$CH3_BEFORE after=$CH3_AFTER)"
|
|
993
|
+
fi
|
|
994
|
+
|
|
995
|
+
echo ""
|
|
996
|
+
|
|
997
|
+
# ─── opencode --global: OG1: seed user key → install --global → key survives + $schema + stamp ─
|
|
998
|
+
echo "=== Install Merge-Aware Tests (--global runtimes) ==="
|
|
999
|
+
echo ""
|
|
1000
|
+
echo "--- OG1: opencode --global — seed user key → user key survives + \$schema + no spurious hooks + stamp ---"
|
|
1001
|
+
|
|
1002
|
+
OG1_CONFIG_DIR="$TMPDIR_EVAL/opencode-global-og1"
|
|
1003
|
+
mkdir -p "$OG1_CONFIG_DIR"
|
|
1004
|
+
OG1_CONFIG_FILE="$OG1_CONFIG_DIR/opencode.json"
|
|
1005
|
+
|
|
1006
|
+
# Seed the global opencode.json with a user key
|
|
1007
|
+
cat > "$OG1_CONFIG_FILE" << 'JSON'
|
|
1008
|
+
{
|
|
1009
|
+
"model": "og1-user-model",
|
|
1010
|
+
"myUserKey": "og1-preserved"
|
|
1011
|
+
}
|
|
1012
|
+
JSON
|
|
1013
|
+
|
|
1014
|
+
# Run init --global --runtime opencode, using env override for path isolation
|
|
1015
|
+
FLOW_AGENTS_USER_OPENCODE_CONFIG="$OG1_CONFIG_FILE" node "$ROOT_DIR/build/src/cli.js" init --runtime opencode --global --yes >/dev/null 2>&1 || true
|
|
1016
|
+
|
|
1017
|
+
# Assert: user key survived
|
|
1018
|
+
if node - "$OG1_CONFIG_FILE" << 'NODE'
|
|
1019
|
+
const fs = require("node:fs");
|
|
1020
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1021
|
+
if (s.model !== "og1-user-model") throw new Error("user key 'model' clobbered: " + JSON.stringify(s));
|
|
1022
|
+
if (s.myUserKey !== "og1-preserved") throw new Error("user key 'myUserKey' clobbered: " + JSON.stringify(s));
|
|
1023
|
+
console.log("ok");
|
|
1024
|
+
NODE
|
|
1025
|
+
then
|
|
1026
|
+
_pass "OG1: opencode --global: user keys survived merge"
|
|
1027
|
+
else
|
|
1028
|
+
_fail "OG1: opencode --global: user keys were clobbered"
|
|
1029
|
+
fi
|
|
1030
|
+
|
|
1031
|
+
# Assert: $schema present
|
|
1032
|
+
if node - "$OG1_CONFIG_FILE" << 'NODE'
|
|
1033
|
+
const fs = require("node:fs");
|
|
1034
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1035
|
+
if (s["$schema"] !== "https://opencode.ai/config.json") throw new Error("\$schema missing or wrong: " + JSON.stringify(s));
|
|
1036
|
+
console.log("ok");
|
|
1037
|
+
NODE
|
|
1038
|
+
then
|
|
1039
|
+
_pass "OG1: opencode --global: \$schema present after merge"
|
|
1040
|
+
else
|
|
1041
|
+
_fail "OG1: opencode --global: \$schema missing after merge"
|
|
1042
|
+
fi
|
|
1043
|
+
|
|
1044
|
+
# Assert: no spurious empty hooks key
|
|
1045
|
+
if node - "$OG1_CONFIG_FILE" << 'NODE'
|
|
1046
|
+
const fs = require("node:fs");
|
|
1047
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1048
|
+
if ("hooks" in s) throw new Error("spurious empty hooks key found: " + JSON.stringify(s));
|
|
1049
|
+
console.log("ok");
|
|
1050
|
+
NODE
|
|
1051
|
+
then
|
|
1052
|
+
_pass "OG1: opencode --global: no spurious empty hooks key"
|
|
1053
|
+
else
|
|
1054
|
+
_fail "OG1: opencode --global: spurious hooks key was injected"
|
|
1055
|
+
fi
|
|
1056
|
+
|
|
1057
|
+
# Assert: version stamp written
|
|
1058
|
+
if node - "$OG1_CONFIG_DIR/.flow-agents/install.json" << 'NODE'
|
|
1059
|
+
const fs = require("node:fs");
|
|
1060
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1061
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
1062
|
+
if (!record.installedAt) throw new Error("install.json missing installedAt");
|
|
1063
|
+
if (record.runtime !== "opencode") throw new Error("wrong runtime: " + record.runtime);
|
|
1064
|
+
if (record.global !== true) throw new Error("global flag not set in stamp");
|
|
1065
|
+
console.log("ok: version=" + record.version);
|
|
1066
|
+
NODE
|
|
1067
|
+
then
|
|
1068
|
+
_pass "OG1: opencode --global: version stamp written (runtime=opencode, global=true)"
|
|
1069
|
+
else
|
|
1070
|
+
_fail "OG1: opencode --global: version stamp missing or wrong"
|
|
1071
|
+
fi
|
|
1072
|
+
|
|
1073
|
+
echo ""
|
|
1074
|
+
|
|
1075
|
+
# ─── codex --global: CG1: FA hooks + stamp present ───────────────────────────
|
|
1076
|
+
echo "--- CG1: codex --global routes to codex-home — FA hooks + stamp present ---"
|
|
1077
|
+
|
|
1078
|
+
CG1_DEST="$TMPDIR_EVAL/codex-global-cg1"
|
|
1079
|
+
mkdir -p "$CG1_DEST"
|
|
1080
|
+
|
|
1081
|
+
# Run init --global --runtime codex with dest override (sandbox isolation)
|
|
1082
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" node "$ROOT_DIR/build/src/cli.js" init --runtime codex --global --dest "$CG1_DEST" --yes >/dev/null 2>&1 || true
|
|
1083
|
+
|
|
1084
|
+
# Assert: FA hooks present in $DEST/hooks.json
|
|
1085
|
+
if node - "$CG1_DEST/hooks.json" << 'NODE'
|
|
1086
|
+
const fs = require("node:fs");
|
|
1087
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1088
|
+
const hooks = s.hooks || {};
|
|
1089
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
1090
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
1091
|
+
);
|
|
1092
|
+
if (!hasFA) throw new Error("FA hooks not found in codex-global hooks.json");
|
|
1093
|
+
console.log("ok");
|
|
1094
|
+
NODE
|
|
1095
|
+
then
|
|
1096
|
+
_pass "CG1: codex --global: FA hooks present in codex-home hooks.json"
|
|
1097
|
+
else
|
|
1098
|
+
_fail "CG1: codex --global: FA hooks missing from codex-home hooks.json"
|
|
1099
|
+
fi
|
|
1100
|
+
|
|
1101
|
+
# Assert: version stamp written
|
|
1102
|
+
if node - "$CG1_DEST/.flow-agents/install.json" << 'NODE'
|
|
1103
|
+
const fs = require("node:fs");
|
|
1104
|
+
const record = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1105
|
+
if (!record.version) throw new Error("install.json missing version");
|
|
1106
|
+
if (record.runtime !== "codex") throw new Error("wrong runtime: " + record.runtime);
|
|
1107
|
+
console.log("ok: version=" + record.version + " runtime=" + record.runtime);
|
|
1108
|
+
NODE
|
|
1109
|
+
then
|
|
1110
|
+
_pass "CG1: codex --global: version stamp written (runtime=codex)"
|
|
1111
|
+
else
|
|
1112
|
+
_fail "CG1: codex --global: version stamp missing or wrong"
|
|
1113
|
+
fi
|
|
1114
|
+
|
|
1115
|
+
echo ""
|
|
1116
|
+
|
|
1117
|
+
# ─── codex --global: CG2: fresh install clean ────────────────────────────────
|
|
1118
|
+
echo "--- CG2: codex --global fresh install — clean codex-home ---"
|
|
1119
|
+
|
|
1120
|
+
CG2_DEST="$TMPDIR_EVAL/codex-global-cg2"
|
|
1121
|
+
mkdir -p "$CG2_DEST"
|
|
1122
|
+
|
|
1123
|
+
# Fresh install (no prior hooks.json)
|
|
1124
|
+
CODEX_REAL_HOME="$TMPDIR_EVAL/fake-real-codex" node "$ROOT_DIR/build/src/cli.js" init --runtime codex --global --dest "$CG2_DEST" --yes >/dev/null 2>&1 || true
|
|
1125
|
+
|
|
1126
|
+
# Assert: hooks.json exists and has FA hooks
|
|
1127
|
+
if [[ -f "$CG2_DEST/hooks.json" ]] && node - "$CG2_DEST/hooks.json" << 'NODE'
|
|
1128
|
+
const fs = require("node:fs");
|
|
1129
|
+
const s = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
1130
|
+
const hooks = s.hooks || {};
|
|
1131
|
+
if (Object.keys(hooks).length === 0) throw new Error("No hooks in fresh codex-global install");
|
|
1132
|
+
const hasFA = Object.values(hooks).flat().some(
|
|
1133
|
+
(g) => (g.hooks || []).some((h) => String(h.statusMessage || "").includes("Recording Flow Agents telemetry"))
|
|
1134
|
+
);
|
|
1135
|
+
if (!hasFA) throw new Error("FA hooks not found in fresh codex-global hooks.json");
|
|
1136
|
+
console.log("ok");
|
|
1137
|
+
NODE
|
|
1138
|
+
then
|
|
1139
|
+
_pass "CG2: codex --global fresh install: hooks.json present with FA hooks"
|
|
1140
|
+
else
|
|
1141
|
+
_fail "CG2: codex --global fresh install: hooks.json missing or FA hooks absent"
|
|
1142
|
+
fi
|
|
1143
|
+
|
|
1144
|
+
echo ""
|
|
1145
|
+
|
|
1146
|
+
# ─── pi --global: PG1: warns NOT_VERIFIED + falls back to workspace default ──
|
|
1147
|
+
echo "--- PG1: pi --global warns NOT_VERIFIED + falls back to workspace default ---"
|
|
1148
|
+
|
|
1149
|
+
PG1_DEST="$TMPDIR_EVAL/pi-global-pg1"
|
|
1150
|
+
mkdir -p "$PG1_DEST"
|
|
1151
|
+
|
|
1152
|
+
# Capture stderr to check for the NOT_VERIFIED warning
|
|
1153
|
+
PG1_STDERR=$(node "$ROOT_DIR/build/src/cli.js" init --runtime pi --global --dest "$PG1_DEST" --yes 2>&1 >/dev/null || true)
|
|
1154
|
+
|
|
1155
|
+
# Assert: stderr contains NOT_VERIFIED warn
|
|
1156
|
+
if echo "$PG1_STDERR" | grep -q "NOT_VERIFIED"; then
|
|
1157
|
+
_pass "PG1: pi --global: stderr contains NOT_VERIFIED warning"
|
|
1158
|
+
else
|
|
1159
|
+
_fail "PG1: pi --global: NOT_VERIFIED warning not found in stderr (got: $PG1_STDERR)"
|
|
1160
|
+
fi
|
|
1161
|
+
|
|
1162
|
+
# Assert: install still ran (bundle files present at dest)
|
|
1163
|
+
if [[ -d "$PG1_DEST" ]] && [[ -f "$PG1_DEST/.flow-agents/install.json" ]] || [[ -d "$PG1_DEST" ]]; then
|
|
1164
|
+
_pass "PG1: pi --global: dest directory exists (fell back to workspace default install)"
|
|
1165
|
+
else
|
|
1166
|
+
_fail "PG1: pi --global: dest directory missing (fallback install did not run)"
|
|
1167
|
+
fi
|
|
1168
|
+
|
|
1169
|
+
echo ""
|
|
1170
|
+
|
|
1171
|
+
echo ""
|
|
1172
|
+
echo "==========================="
|
|
1173
|
+
total=$((pass + fail))
|
|
1174
|
+
echo "Results: ${pass}/${total} passed, ${fail} failed"
|
|
1175
|
+
[[ "$fail" -gt 0 ]] && exit 1
|
|
1176
|
+
exit 0
|