@rune-kit/rune 2.10.0 → 2.12.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/LICENSE +21 -21
- package/README.md +65 -6
- package/commands/rune.md +168 -168
- package/compiler/__tests__/detect-invariants.test.js +136 -0
- package/compiler/__tests__/doctor-mesh.test.js +229 -0
- package/compiler/__tests__/hook-dispatch.test.js +91 -0
- package/compiler/__tests__/hooks-antigravity.test.js +118 -0
- package/compiler/__tests__/hooks-cursor.test.js +139 -0
- package/compiler/__tests__/hooks-install.test.js +305 -0
- package/compiler/__tests__/hooks-merge.test.js +204 -0
- package/compiler/__tests__/hooks-tiers.test.js +519 -0
- package/compiler/__tests__/hooks-windsurf.test.js +115 -0
- package/compiler/__tests__/inject-claude-md.test.js +152 -0
- package/compiler/__tests__/load-invariants.test.js +408 -0
- package/compiler/__tests__/onboard-invariants.test.js +240 -0
- package/compiler/adapters/hooks/antigravity.js +140 -0
- package/compiler/adapters/hooks/claude.js +166 -0
- package/compiler/adapters/hooks/cursor.js +191 -0
- package/compiler/adapters/hooks/index.js +82 -0
- package/compiler/adapters/hooks/tier-emitter.js +182 -0
- package/compiler/adapters/hooks/windsurf.js +202 -0
- package/compiler/bin/rune.js +196 -6
- package/compiler/commands/hook-dispatch.js +87 -0
- package/compiler/commands/hooks/install.js +120 -0
- package/compiler/commands/hooks/merge.js +211 -0
- package/compiler/commands/hooks/presets.js +116 -0
- package/compiler/commands/hooks/status.js +112 -0
- package/compiler/commands/hooks/tiers.js +221 -0
- package/compiler/commands/hooks/uninstall.js +94 -0
- package/compiler/doctor.js +236 -0
- package/contexts/dev.md +34 -34
- package/contexts/research.md +43 -43
- package/contexts/review.md +55 -55
- package/extensions/ai-ml/PACK.md +88 -88
- package/extensions/ai-ml/skills/ai-agents.md +172 -172
- package/extensions/ai-ml/skills/code-sandbox.md +187 -187
- package/extensions/ai-ml/skills/deep-research.md +146 -146
- package/extensions/ai-ml/skills/embedding-search.md +66 -66
- package/extensions/ai-ml/skills/fine-tuning-guide.md +74 -74
- package/extensions/ai-ml/skills/llm-architect.md +125 -125
- package/extensions/ai-ml/skills/llm-integration.md +64 -64
- package/extensions/ai-ml/skills/prompt-patterns.md +72 -72
- package/extensions/ai-ml/skills/rag-patterns.md +66 -66
- package/extensions/ai-ml/skills/web-extraction.md +114 -114
- package/extensions/analytics/PACK.md +92 -92
- package/extensions/analytics/skills/ab-testing.md +72 -72
- package/extensions/analytics/skills/dashboard-patterns.md +83 -83
- package/extensions/analytics/skills/data-validation.md +68 -68
- package/extensions/analytics/skills/funnel-analysis.md +81 -81
- package/extensions/analytics/skills/sql-patterns.md +57 -57
- package/extensions/analytics/skills/statistical-analysis.md +79 -79
- package/extensions/analytics/skills/tracking-setup.md +71 -71
- package/extensions/backend/PACK.md +104 -104
- package/extensions/backend/skills/api-patterns.md +84 -84
- package/extensions/backend/skills/async-pipeline.md +193 -193
- package/extensions/backend/skills/auth-patterns.md +97 -97
- package/extensions/backend/skills/background-jobs.md +133 -133
- package/extensions/backend/skills/caching-patterns.md +108 -108
- package/extensions/backend/skills/cli-generation.md +133 -133
- package/extensions/backend/skills/database-patterns.md +87 -87
- package/extensions/backend/skills/middleware-patterns.md +104 -104
- package/extensions/chrome-ext/PACK.md +93 -93
- package/extensions/chrome-ext/skills/cws-preflight.md +143 -143
- package/extensions/chrome-ext/skills/cws-publish.md +104 -104
- package/extensions/chrome-ext/skills/ext-ai-integration.md +251 -251
- package/extensions/chrome-ext/skills/ext-messaging.md +139 -139
- package/extensions/chrome-ext/skills/ext-storage.md +133 -133
- package/extensions/chrome-ext/skills/mv3-scaffold.md +164 -164
- package/extensions/content/PACK.md +96 -96
- package/extensions/content/skills/blog-patterns.md +88 -88
- package/extensions/content/skills/cms-integration.md +131 -131
- package/extensions/content/skills/content-scoring.md +107 -107
- package/extensions/content/skills/i18n.md +83 -83
- package/extensions/content/skills/mdx-authoring.md +137 -137
- package/extensions/content/skills/reference.md +1014 -1014
- package/extensions/content/skills/seo-patterns.md +67 -67
- package/extensions/content/skills/video-repurpose.md +153 -153
- package/extensions/devops/PACK.md +101 -101
- package/extensions/devops/skills/chaos-testing.md +67 -67
- package/extensions/devops/skills/ci-cd.md +75 -75
- package/extensions/devops/skills/docker.md +58 -58
- package/extensions/devops/skills/edge-serverless.md +163 -163
- package/extensions/devops/skills/infra-as-code.md +158 -158
- package/extensions/devops/skills/kubernetes.md +110 -110
- package/extensions/devops/skills/monitoring.md +57 -57
- package/extensions/devops/skills/server-setup.md +64 -64
- package/extensions/devops/skills/ssl-domain.md +42 -42
- package/extensions/ecommerce/PACK.md +116 -116
- package/extensions/ecommerce/skills/cart-system.md +79 -79
- package/extensions/ecommerce/skills/inventory-mgmt.md +102 -102
- package/extensions/ecommerce/skills/order-management.md +126 -126
- package/extensions/ecommerce/skills/payment-integration.md +472 -472
- package/extensions/ecommerce/skills/shopify-dev.md +69 -69
- package/extensions/ecommerce/skills/subscription-billing.md +93 -93
- package/extensions/ecommerce/skills/tax-compliance.md +117 -117
- package/extensions/gamedev/PACK.md +142 -142
- package/extensions/gamedev/skills/asset-pipeline.md +74 -74
- package/extensions/gamedev/skills/audio-system.md +129 -129
- package/extensions/gamedev/skills/camera-system.md +87 -87
- package/extensions/gamedev/skills/ecs.md +98 -98
- package/extensions/gamedev/skills/game-loops.md +72 -72
- package/extensions/gamedev/skills/input-system.md +199 -199
- package/extensions/gamedev/skills/multiplayer.md +180 -180
- package/extensions/gamedev/skills/particles.md +105 -105
- package/extensions/gamedev/skills/physics-engine.md +89 -89
- package/extensions/gamedev/skills/scene-management.md +146 -146
- package/extensions/gamedev/skills/threejs-patterns.md +90 -90
- package/extensions/gamedev/skills/webgl.md +71 -71
- package/extensions/mobile/PACK.md +106 -106
- package/extensions/mobile/skills/app-store-connect.md +152 -152
- package/extensions/mobile/skills/app-store-prep.md +66 -66
- package/extensions/mobile/skills/deep-linking.md +109 -109
- package/extensions/mobile/skills/flutter.md +60 -60
- package/extensions/mobile/skills/ios-build-pipeline.md +142 -142
- package/extensions/mobile/skills/native-bridge.md +66 -66
- package/extensions/mobile/skills/ota-updates.md +97 -97
- package/extensions/mobile/skills/push-notifications.md +111 -111
- package/extensions/mobile/skills/react-native.md +82 -82
- package/extensions/saas/PACK.md +116 -116
- package/extensions/saas/skills/billing-integration.md +200 -200
- package/extensions/saas/skills/feature-flags.md +130 -130
- package/extensions/saas/skills/multi-tenant.md +103 -103
- package/extensions/saas/skills/onboarding-flow.md +139 -139
- package/extensions/saas/skills/subscription-flow.md +95 -95
- package/extensions/saas/skills/team-management.md +144 -144
- package/extensions/security/PACK.md +99 -99
- package/extensions/security/skills/api-security.md +140 -140
- package/extensions/security/skills/compliance.md +68 -68
- package/extensions/security/skills/owasp-audit.md +64 -64
- package/extensions/security/skills/pentest-patterns.md +77 -77
- package/extensions/security/skills/secret-mgmt.md +65 -65
- package/extensions/security/skills/supply-chain.md +65 -65
- package/extensions/trading/PACK.md +80 -80
- package/extensions/trading/skills/chart-components.md +55 -55
- package/extensions/trading/skills/experiment-loop.md +125 -125
- package/extensions/trading/skills/fintech-patterns.md +47 -47
- package/extensions/trading/skills/indicator-library.md +58 -58
- package/extensions/trading/skills/quant-analysis.md +111 -111
- package/extensions/trading/skills/realtime-data.md +58 -58
- package/extensions/trading/skills/trade-logic.md +104 -104
- package/extensions/ui/PACK.md +130 -130
- package/extensions/ui/skills/a11y-audit.md +91 -91
- package/extensions/ui/skills/animation-patterns.md +127 -127
- package/extensions/ui/skills/component-patterns.md +100 -100
- package/extensions/ui/skills/design-decision.md +108 -108
- package/extensions/ui/skills/design-system.md +68 -68
- package/extensions/ui/skills/landing-patterns.md +155 -155
- package/extensions/ui/skills/palette-picker.md +173 -173
- package/extensions/ui/skills/react-health.md +90 -90
- package/extensions/ui/skills/type-system.md +125 -125
- package/extensions/ui/skills/web-vitals.md +153 -153
- package/extensions/zalo/PACK.md +145 -145
- package/extensions/zalo/skills/zalo-oa-mcp.md +317 -317
- package/extensions/zalo/skills/zalo-oa-messaging.md +429 -429
- package/extensions/zalo/skills/zalo-oa-setup.md +236 -236
- package/extensions/zalo/skills/zalo-oa-webhook.md +189 -189
- package/extensions/zalo/skills/zalo-personal-messaging.md +194 -194
- package/extensions/zalo/skills/zalo-personal-setup.md +153 -153
- package/extensions/zalo/skills/zalo-rate-guard.md +219 -219
- package/hooks/auto-format/index.cjs +48 -48
- package/hooks/hooks.json +111 -111
- package/hooks/post-session-reflect/index.cjs +189 -189
- package/hooks/pre-compact/index.cjs +95 -95
- package/hooks/run-hook.cmd +1 -1
- package/hooks/secrets-scan/index.cjs +100 -100
- package/hooks/session-start/index.cjs +71 -71
- package/hooks/typecheck/index.cjs +65 -65
- package/package.json +63 -63
- package/references/ui-pro-max-data/LICENSE-UI-PRO-MAX +21 -21
- package/references/ui-pro-max-data/charts.csv +26 -26
- package/references/ui-pro-max-data/colors.csv +161 -161
- package/references/ui-pro-max-data/styles.csv +68 -68
- package/references/ui-pro-max-data/typography.csv +74 -74
- package/references/ui-pro-max-data/ui-reasoning.csv +162 -162
- package/references/ui-pro-max-data/ux-guidelines.csv +99 -99
- package/skills/adversary/SKILL.md +283 -283
- package/skills/asset-creator/SKILL.md +157 -157
- package/skills/audit/SKILL.md +147 -2
- package/skills/autopsy/SKILL.md +335 -335
- package/skills/ba/SKILL.md +85 -1
- package/skills/brainstorm/SKILL.md +380 -342
- package/skills/browser-pilot/SKILL.md +169 -168
- package/skills/constraint-check/SKILL.md +165 -165
- package/skills/context-engine/SKILL.md +408 -404
- package/skills/cook/SKILL.md +917 -863
- package/skills/db/SKILL.md +273 -273
- package/skills/debug/SKILL.md +465 -465
- package/skills/dependency-doctor/SKILL.md +265 -235
- package/skills/deploy/SKILL.md +274 -231
- package/skills/design/DESIGN-REFERENCE.md +365 -365
- package/skills/design/SKILL.md +590 -589
- package/skills/doc-processor/SKILL.md +254 -254
- package/skills/docs/SKILL.md +374 -374
- package/skills/docs-seeker/SKILL.md +178 -177
- package/skills/fix/SKILL.md +332 -330
- package/skills/git/SKILL.md +339 -339
- package/skills/hallucination-guard/SKILL.md +220 -219
- package/skills/incident/SKILL.md +254 -253
- package/skills/integrity-check/SKILL.md +169 -169
- package/skills/journal/SKILL.md +241 -240
- package/skills/launch/SKILL.md +344 -344
- package/skills/logic-guardian/SKILL.md +269 -251
- package/skills/marketing/SKILL.md +351 -289
- package/skills/mcp-builder/SKILL.md +425 -425
- package/skills/neural-memory/SKILL.md +359 -362
- package/skills/onboard/SKILL.md +432 -403
- package/skills/onboard/references/invariants-template.md +76 -0
- package/skills/onboard/scripts/detect-invariants.js +439 -0
- package/skills/onboard/scripts/inject-claude-md.js +150 -0
- package/skills/onboard/scripts/onboard-invariants.js +194 -0
- package/skills/perf/SKILL.md +347 -346
- package/skills/plan/SKILL.md +435 -428
- package/skills/preflight/SKILL.md +415 -415
- package/skills/problem-solver/SKILL.md +380 -284
- package/skills/rescue/SKILL.md +474 -474
- package/skills/research/SKILL.md +4 -0
- package/skills/retro/SKILL.md +3 -1
- package/skills/review/SKILL.md +614 -588
- package/skills/review-intake/SKILL.md +249 -249
- package/skills/safeguard/SKILL.md +200 -200
- package/skills/sast/SKILL.md +190 -190
- package/skills/scaffold/SKILL.md +328 -287
- package/skills/scope-guard/SKILL.md +183 -180
- package/skills/scout/SKILL.md +269 -263
- package/skills/sentinel/SKILL.md +384 -381
- package/skills/sentinel-env/SKILL.md +254 -254
- package/skills/sequential-thinking/SKILL.md +234 -234
- package/skills/session-bridge/SKILL.md +595 -543
- package/skills/session-bridge/scripts/load-invariants.js +397 -0
- package/skills/skill-forge/SKILL.md +581 -581
- package/skills/skill-router/SKILL.md +3 -0
- package/skills/slides/SKILL.md +19 -0
- package/skills/surgeon/SKILL.md +215 -215
- package/skills/team/SKILL.md +557 -537
- package/skills/test/SKILL.md +620 -614
- package/skills/trend-scout/SKILL.md +145 -145
- package/skills/verification/SKILL.md +334 -326
- package/skills/video-creator/SKILL.md +201 -201
- package/skills/watchdog/SKILL.md +168 -168
- package/skills/worktree/SKILL.md +140 -140
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for doctor mesh integrity checks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import assert from 'node:assert';
|
|
6
|
+
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { checkMeshIntegrity, formatMeshResults } from '../doctor.js';
|
|
11
|
+
|
|
12
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
const TEST_ROOT = path.join(__dirname, '.test-mesh-skills');
|
|
14
|
+
|
|
15
|
+
describe('checkMeshIntegrity', () => {
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
await mkdir(path.join(TEST_ROOT, 'skills', 'skill-a'), { recursive: true });
|
|
18
|
+
await mkdir(path.join(TEST_ROOT, 'skills', 'skill-b'), { recursive: true });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await rm(TEST_ROOT, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('detects missing reciprocal connections', async () => {
|
|
26
|
+
// skill-a calls skill-b, but skill-b doesn't acknowledge
|
|
27
|
+
await writeFile(
|
|
28
|
+
path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
|
|
29
|
+
`---
|
|
30
|
+
name: skill-a
|
|
31
|
+
metadata:
|
|
32
|
+
version: "1.0.0"
|
|
33
|
+
---
|
|
34
|
+
# skill-a
|
|
35
|
+
## Calls (outbound)
|
|
36
|
+
- \`skill-b\` (L2): some reason
|
|
37
|
+
## Called By (inbound)
|
|
38
|
+
None
|
|
39
|
+
## Sharp Edges
|
|
40
|
+
| Mode | Sev | Mit |
|
|
41
|
+
## Done When
|
|
42
|
+
- done
|
|
43
|
+
## Cost Profile
|
|
44
|
+
~100 tokens
|
|
45
|
+
`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
await writeFile(
|
|
49
|
+
path.join(TEST_ROOT, 'skills', 'skill-b', 'SKILL.md'),
|
|
50
|
+
`---
|
|
51
|
+
name: skill-b
|
|
52
|
+
metadata:
|
|
53
|
+
version: "1.0.0"
|
|
54
|
+
---
|
|
55
|
+
# skill-b
|
|
56
|
+
## Calls (outbound)
|
|
57
|
+
None
|
|
58
|
+
## Called By (inbound)
|
|
59
|
+
None
|
|
60
|
+
## Sharp Edges
|
|
61
|
+
| Mode | Sev | Mit |
|
|
62
|
+
## Done When
|
|
63
|
+
- done
|
|
64
|
+
## Cost Profile
|
|
65
|
+
~100 tokens
|
|
66
|
+
`,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
const results = await checkMeshIntegrity(TEST_ROOT);
|
|
70
|
+
|
|
71
|
+
assert.strictEqual(results.stats.skills, 2, 'Should find 2 skills');
|
|
72
|
+
assert.strictEqual(results.stats.missingReciprocals, 1, 'Should detect 1 missing reciprocal');
|
|
73
|
+
assert.ok(
|
|
74
|
+
results.warnings.some((w) => w.includes('skill-b') && w.includes('skill-a')),
|
|
75
|
+
'Should warn that skill-b is missing skill-a in Called By',
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('passes when reciprocals are correct', async () => {
|
|
80
|
+
await writeFile(
|
|
81
|
+
path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
|
|
82
|
+
`---
|
|
83
|
+
name: skill-a
|
|
84
|
+
metadata:
|
|
85
|
+
version: "1.0.0"
|
|
86
|
+
---
|
|
87
|
+
# skill-a
|
|
88
|
+
## Calls (outbound)
|
|
89
|
+
- \`skill-b\` (L2): some reason
|
|
90
|
+
## Called By (inbound)
|
|
91
|
+
None
|
|
92
|
+
## Sharp Edges
|
|
93
|
+
| Mode | Sev | Mit |
|
|
94
|
+
## Done When
|
|
95
|
+
- done
|
|
96
|
+
## Cost Profile
|
|
97
|
+
~100 tokens
|
|
98
|
+
`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await writeFile(
|
|
102
|
+
path.join(TEST_ROOT, 'skills', 'skill-b', 'SKILL.md'),
|
|
103
|
+
`---
|
|
104
|
+
name: skill-b
|
|
105
|
+
metadata:
|
|
106
|
+
version: "1.0.0"
|
|
107
|
+
---
|
|
108
|
+
# skill-b
|
|
109
|
+
## Calls (outbound)
|
|
110
|
+
None
|
|
111
|
+
## Called By (inbound)
|
|
112
|
+
- \`skill-a\` (L2): caller
|
|
113
|
+
## Sharp Edges
|
|
114
|
+
| Mode | Sev | Mit |
|
|
115
|
+
## Done When
|
|
116
|
+
- done
|
|
117
|
+
## Cost Profile
|
|
118
|
+
~100 tokens
|
|
119
|
+
`,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const results = await checkMeshIntegrity(TEST_ROOT);
|
|
123
|
+
|
|
124
|
+
assert.strictEqual(results.stats.missingReciprocals, 0, 'Should have no missing reciprocals');
|
|
125
|
+
assert.strictEqual(
|
|
126
|
+
results.checks.find((c) => c.name === 'Reciprocal connections')?.status,
|
|
127
|
+
'pass',
|
|
128
|
+
'Reciprocal check should pass',
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('suggests version bump for mature skills', async () => {
|
|
133
|
+
await writeFile(
|
|
134
|
+
path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
|
|
135
|
+
`---
|
|
136
|
+
name: skill-a
|
|
137
|
+
metadata:
|
|
138
|
+
version: "0.9.0"
|
|
139
|
+
---
|
|
140
|
+
# skill-a
|
|
141
|
+
## Calls (outbound)
|
|
142
|
+
None
|
|
143
|
+
## Called By (inbound)
|
|
144
|
+
None
|
|
145
|
+
## Sharp Edges
|
|
146
|
+
| Mode | Sev | Mit |
|
|
147
|
+
## Done When
|
|
148
|
+
- done
|
|
149
|
+
## Cost Profile
|
|
150
|
+
~100 tokens
|
|
151
|
+
## Returns
|
|
152
|
+
| Artifact | Format | Location |
|
|
153
|
+
`,
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const results = await checkMeshIntegrity(TEST_ROOT);
|
|
157
|
+
|
|
158
|
+
assert.ok(
|
|
159
|
+
results.warnings.some((w) => w.includes('skill-a') && w.includes('1.0')),
|
|
160
|
+
'Should suggest promoting to 1.0',
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('detects missing required sections', async () => {
|
|
165
|
+
await writeFile(
|
|
166
|
+
path.join(TEST_ROOT, 'skills', 'skill-a', 'SKILL.md'),
|
|
167
|
+
`---
|
|
168
|
+
name: skill-a
|
|
169
|
+
metadata:
|
|
170
|
+
version: "1.0.0"
|
|
171
|
+
---
|
|
172
|
+
# skill-a
|
|
173
|
+
## Calls (outbound)
|
|
174
|
+
None
|
|
175
|
+
## Called By (inbound)
|
|
176
|
+
None
|
|
177
|
+
`,
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
const results = await checkMeshIntegrity(TEST_ROOT);
|
|
181
|
+
|
|
182
|
+
assert.ok(
|
|
183
|
+
results.warnings.some((w) => w.includes('skill-a') && w.includes('Sharp Edges')),
|
|
184
|
+
'Should warn about missing Sharp Edges',
|
|
185
|
+
);
|
|
186
|
+
assert.ok(
|
|
187
|
+
results.warnings.some((w) => w.includes('skill-a') && w.includes('Done When')),
|
|
188
|
+
'Should warn about missing Done When',
|
|
189
|
+
);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('formatMeshResults', () => {
|
|
194
|
+
test('formats results for console output', () => {
|
|
195
|
+
const results = {
|
|
196
|
+
checks: [
|
|
197
|
+
{ name: 'Reciprocal connections', status: 'pass' },
|
|
198
|
+
{ name: 'Version maturity', status: 'warn', detail: '2 skills ready for 1.0' },
|
|
199
|
+
],
|
|
200
|
+
warnings: ['skill-a v0.9.0: consider promoting to 1.0'],
|
|
201
|
+
errors: [],
|
|
202
|
+
stats: { skills: 10, connections: 50, missingReciprocals: 0 },
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const output = formatMeshResults(results);
|
|
206
|
+
|
|
207
|
+
assert.ok(output.includes('Skills: 10'), 'Should show skill count');
|
|
208
|
+
assert.ok(output.includes('Connections: 50'), 'Should show connection count');
|
|
209
|
+
assert.ok(output.includes('[✓] Reciprocal'), 'Should show pass icon');
|
|
210
|
+
assert.ok(output.includes('[!] Version'), 'Should show warn icon');
|
|
211
|
+
assert.ok(output.includes('skill-a v0.9.0'), 'Should include warnings');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
describe('integration: real skills', () => {
|
|
216
|
+
test('runs mesh check on actual Rune skills directory', async () => {
|
|
217
|
+
const runeRoot = path.resolve(__dirname, '../..');
|
|
218
|
+
const results = await checkMeshIntegrity(runeRoot);
|
|
219
|
+
|
|
220
|
+
// Should find skills
|
|
221
|
+
assert.ok(results.stats.skills > 50, `Should find 50+ skills, got ${results.stats.skills}`);
|
|
222
|
+
|
|
223
|
+
// Should find connections
|
|
224
|
+
assert.ok(results.stats.connections > 100, `Should find 100+ connections, got ${results.stats.connections}`);
|
|
225
|
+
|
|
226
|
+
// Should not have errors
|
|
227
|
+
assert.strictEqual(results.errors.length, 0, `Should have no errors: ${results.errors.join(', ')}`);
|
|
228
|
+
});
|
|
229
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { PassThrough } from 'node:stream';
|
|
3
|
+
import { describe, test } from 'node:test';
|
|
4
|
+
import { dispatchHook } from '../commands/hook-dispatch.js';
|
|
5
|
+
|
|
6
|
+
function makeIO({ stdinData = '' } = {}) {
|
|
7
|
+
const stdin = new PassThrough();
|
|
8
|
+
stdin.setEncoding = () => {};
|
|
9
|
+
stdin.isTTY = false;
|
|
10
|
+
if (stdinData) {
|
|
11
|
+
stdin.write(stdinData);
|
|
12
|
+
}
|
|
13
|
+
stdin.end();
|
|
14
|
+
|
|
15
|
+
const stdout = new PassThrough();
|
|
16
|
+
const stderr = new PassThrough();
|
|
17
|
+
let out = '';
|
|
18
|
+
let err = '';
|
|
19
|
+
stdout.on('data', (chunk) => {
|
|
20
|
+
out += chunk.toString();
|
|
21
|
+
});
|
|
22
|
+
stderr.on('data', (chunk) => {
|
|
23
|
+
err += chunk.toString();
|
|
24
|
+
});
|
|
25
|
+
return { stdin, stdout, stderr, getOut: () => out, getErr: () => err };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe('dispatchHook', () => {
|
|
29
|
+
test('unknown skill exits 1 in strict mode', async () => {
|
|
30
|
+
const io = makeIO();
|
|
31
|
+
const code = await dispatchHook(['evil-skill'], io);
|
|
32
|
+
assert.strictEqual(code, 1);
|
|
33
|
+
assert.match(io.getErr(), /unknown skill/);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('unknown skill exits 0 in gentle mode', async () => {
|
|
37
|
+
const io = makeIO();
|
|
38
|
+
const code = await dispatchHook(['evil-skill', '--gentle'], io);
|
|
39
|
+
assert.strictEqual(code, 0);
|
|
40
|
+
assert.match(io.getErr(), /unknown skill/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('missing skill name errors', async () => {
|
|
44
|
+
const io = makeIO();
|
|
45
|
+
const code = await dispatchHook([], io);
|
|
46
|
+
assert.strictEqual(code, 1);
|
|
47
|
+
assert.match(io.getErr(), /missing skill/);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('known skill succeeds and emits advisory line', async () => {
|
|
51
|
+
const io = makeIO({ stdinData: JSON.stringify({ tool_name: 'Edit' }) });
|
|
52
|
+
const code = await dispatchHook(['preflight', '--gentle'], io);
|
|
53
|
+
assert.strictEqual(code, 0);
|
|
54
|
+
assert.match(io.getOut(), /rune-hook: preflight/);
|
|
55
|
+
assert.match(io.getOut(), /advisory/);
|
|
56
|
+
assert.match(io.getOut(), /Edit/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('strict mode prints enforcing label', async () => {
|
|
60
|
+
const io = makeIO({ stdinData: '{}' });
|
|
61
|
+
const code = await dispatchHook(['sentinel'], io);
|
|
62
|
+
assert.strictEqual(code, 0);
|
|
63
|
+
assert.match(io.getOut(), /enforcing/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('malformed stdin does not crash', async () => {
|
|
67
|
+
const io = makeIO({ stdinData: 'not json at all' });
|
|
68
|
+
const code = await dispatchHook(['completion-gate', '--gentle'], io);
|
|
69
|
+
assert.strictEqual(code, 0);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// T5: invalid skill name (not in allowlist) → exits non-zero, stderr contains rejected name
|
|
73
|
+
test('T5: skill not in allowlist exits 1 in strict mode with name in stderr', async () => {
|
|
74
|
+
const io = makeIO();
|
|
75
|
+
const code = await dispatchHook(['totally-fake-skill'], io);
|
|
76
|
+
assert.strictEqual(code, 1);
|
|
77
|
+
assert.ok(io.getErr().includes('totally-fake-skill'), 'stderr must contain the rejected skill name');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// T6: stdin larger than 1MB → truncates or errors gracefully (no OOM)
|
|
81
|
+
test('T6: stdin larger than 1MB is rejected gracefully', async () => {
|
|
82
|
+
// Build a payload just over the 1MB limit
|
|
83
|
+
const big = 'x'.repeat(1_100_000);
|
|
84
|
+
const io = makeIO({ stdinData: big });
|
|
85
|
+
// Should not throw — must return an exit code (0 or 1, gentle or strict)
|
|
86
|
+
const code = await dispatchHook(['preflight', '--gentle'], io);
|
|
87
|
+
assert.ok(typeof code === 'number', 'must return a numeric exit code');
|
|
88
|
+
// gentle mode: still exits 0 even if stdin was discarded
|
|
89
|
+
assert.strictEqual(code, 0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
7
|
+
import * as antigravity from '../adapters/hooks/antigravity.js';
|
|
8
|
+
import { installHooks } from '../commands/hooks/install.js';
|
|
9
|
+
import { hookStatus } from '../commands/hooks/status.js';
|
|
10
|
+
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
11
|
+
|
|
12
|
+
const RUNE_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
13
|
+
const RULES_DIR = '.antigravity/rules';
|
|
14
|
+
|
|
15
|
+
let tmpRoot;
|
|
16
|
+
|
|
17
|
+
async function seedAntigravity(root) {
|
|
18
|
+
await mkdir(path.join(root, '.antigravity'), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-antigravity-'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('antigravity adapter', () => {
|
|
30
|
+
test('detect() true when .antigravity exists', async () => {
|
|
31
|
+
await seedAntigravity(tmpRoot);
|
|
32
|
+
assert.strictEqual(antigravity.detect(tmpRoot), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('emit(gentle) returns 3 rule markdowns with WARN mode', async () => {
|
|
36
|
+
const plan = await antigravity.emit({ preset: 'gentle', projectRoot: tmpRoot });
|
|
37
|
+
assert.strictEqual(plan.files.length, 3);
|
|
38
|
+
for (const file of plan.files) {
|
|
39
|
+
assert.ok(file.content.includes('rune-managed: true'));
|
|
40
|
+
assert.ok(file.content.includes('mode: WARN'));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('emit(strict) renders BLOCK mode', async () => {
|
|
45
|
+
const plan = await antigravity.emit({ preset: 'strict', projectRoot: tmpRoot });
|
|
46
|
+
for (const file of plan.files) {
|
|
47
|
+
assert.ok(file.content.includes('mode: BLOCK'));
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('emit() rejects invalid preset', async () => {
|
|
52
|
+
await assert.rejects(antigravity.emit({ preset: 'weird', projectRoot: tmpRoot }), /invalid preset/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('emit() notes call out experimental status', async () => {
|
|
56
|
+
const plan = await antigravity.emit({ preset: 'gentle', projectRoot: tmpRoot });
|
|
57
|
+
assert.ok(plan.notes.some((n) => n.toLowerCase().includes('experimental')));
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('install writes rule files under .antigravity/rules/', async () => {
|
|
61
|
+
await seedAntigravity(tmpRoot);
|
|
62
|
+
const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
|
|
63
|
+
assert.strictEqual(result.written, true);
|
|
64
|
+
const files = await readdir(path.join(tmpRoot, RULES_DIR));
|
|
65
|
+
assert.ok(files.includes('rune-preflight.md'));
|
|
66
|
+
assert.ok(files.includes('rune-sentinel.md'));
|
|
67
|
+
assert.ok(files.includes('rune-dependency-doctor.md'));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('idempotent re-install', async () => {
|
|
71
|
+
await seedAntigravity(tmpRoot);
|
|
72
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
|
|
73
|
+
const first = await readFile(path.join(tmpRoot, RULES_DIR, 'rune-preflight.md'), 'utf-8');
|
|
74
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
|
|
75
|
+
const second = await readFile(path.join(tmpRoot, RULES_DIR, 'rune-preflight.md'), 'utf-8');
|
|
76
|
+
assert.strictEqual(first, second);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('uninstall removes Rune rules, preserves user rules', async () => {
|
|
80
|
+
await seedAntigravity(tmpRoot);
|
|
81
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
82
|
+
await mkdir(rulesDir, { recursive: true });
|
|
83
|
+
await writeFile(path.join(rulesDir, 'user-rule.md'), '# User rule\n', 'utf-8');
|
|
84
|
+
|
|
85
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'antigravity' });
|
|
86
|
+
await uninstallHooks(tmpRoot, { platform: 'antigravity' });
|
|
87
|
+
|
|
88
|
+
const remaining = await readdir(rulesDir);
|
|
89
|
+
assert.ok(remaining.includes('user-rule.md'), 'user rule must survive');
|
|
90
|
+
assert.ok(!remaining.some((f) => f.startsWith('rune-')), 'no rune-* files should remain');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test('uninstall leaves non-managed rune-* files alone', async () => {
|
|
94
|
+
await seedAntigravity(tmpRoot);
|
|
95
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
96
|
+
await mkdir(rulesDir, { recursive: true });
|
|
97
|
+
await writeFile(path.join(rulesDir, 'rune-fake.md'), '---\ndescription: imposter\n---\n\nnot ours\n', 'utf-8');
|
|
98
|
+
|
|
99
|
+
await uninstallHooks(tmpRoot, { platform: 'antigravity' });
|
|
100
|
+
assert.ok(existsSync(path.join(rulesDir, 'rune-fake.md')));
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('status after install reports wired skills', async () => {
|
|
104
|
+
await seedAntigravity(tmpRoot);
|
|
105
|
+
await installHooks(tmpRoot, { preset: 'strict', platform: 'antigravity' });
|
|
106
|
+
const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'antigravity' });
|
|
107
|
+
const r = result.results.find((x) => x.platform === 'antigravity');
|
|
108
|
+
assert.strictEqual(r.installed, true);
|
|
109
|
+
assert.strictEqual(r.preset, 'strict');
|
|
110
|
+
assert.ok(r.wired.includes('preflight'));
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('status reports not installed when .antigravity missing', async () => {
|
|
114
|
+
const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'antigravity' });
|
|
115
|
+
const r = result.results.find((x) => x.platform === 'antigravity');
|
|
116
|
+
assert.strictEqual(r.installed, false);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import assert from 'node:assert';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { afterEach, beforeEach, describe, test } from 'node:test';
|
|
7
|
+
import * as cursor from '../adapters/hooks/cursor.js';
|
|
8
|
+
import { installHooks } from '../commands/hooks/install.js';
|
|
9
|
+
import { hookStatus } from '../commands/hooks/status.js';
|
|
10
|
+
import { uninstallHooks } from '../commands/hooks/uninstall.js';
|
|
11
|
+
|
|
12
|
+
const RUNE_ROOT = path.resolve(import.meta.dirname, '..', '..');
|
|
13
|
+
const RULES_DIR = '.cursor/rules';
|
|
14
|
+
|
|
15
|
+
let tmpRoot;
|
|
16
|
+
|
|
17
|
+
async function seedCursor(root) {
|
|
18
|
+
await mkdir(path.join(root, '.cursor'), { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
beforeEach(async () => {
|
|
22
|
+
tmpRoot = await mkdtemp(path.join(tmpdir(), 'rune-cursor-'));
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(async () => {
|
|
26
|
+
if (tmpRoot) await rm(tmpRoot, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('cursor adapter', () => {
|
|
30
|
+
test('detect() true when .cursor exists', async () => {
|
|
31
|
+
await seedCursor(tmpRoot);
|
|
32
|
+
assert.strictEqual(cursor.detect(tmpRoot), true);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('detect() false when .cursor missing', () => {
|
|
36
|
+
assert.strictEqual(cursor.detect(tmpRoot), false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('emit(gentle) returns 3 .mdc rule files', async () => {
|
|
40
|
+
const plan = await cursor.emit({ preset: 'gentle', projectRoot: tmpRoot });
|
|
41
|
+
assert.strictEqual(plan.files.length, 3);
|
|
42
|
+
const names = plan.files.map((f) => path.basename(f.path)).sort();
|
|
43
|
+
assert.deepStrictEqual(names, ['rune-dependency-doctor.mdc', 'rune-preflight.mdc', 'rune-sentinel.mdc']);
|
|
44
|
+
for (const file of plan.files) {
|
|
45
|
+
assert.ok(file.content.includes('rune-managed: true'));
|
|
46
|
+
assert.ok(file.content.includes('@rune-kit/rune hook-dispatch'));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test('emit(strict) renders WARN → BLOCK guidance', async () => {
|
|
51
|
+
const gentle = await cursor.emit({ preset: 'gentle', projectRoot: tmpRoot });
|
|
52
|
+
const strict = await cursor.emit({ preset: 'strict', projectRoot: tmpRoot });
|
|
53
|
+
const gentlePreflight = gentle.files.find((f) => f.path.endsWith('rune-preflight.mdc')).content;
|
|
54
|
+
const strictPreflight = strict.files.find((f) => f.path.endsWith('rune-preflight.mdc')).content;
|
|
55
|
+
assert.ok(gentlePreflight.includes('WARN'));
|
|
56
|
+
assert.ok(strictPreflight.includes('BLOCK'));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('emit(off) delegates to uninstall', async () => {
|
|
60
|
+
await seedCursor(tmpRoot);
|
|
61
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
|
|
62
|
+
const plan = await cursor.emit({ preset: 'off', projectRoot: tmpRoot });
|
|
63
|
+
assert.ok(plan.files.every((f) => f.content === null));
|
|
64
|
+
assert.ok(plan.files.length >= 1);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test('emit() rejects invalid preset', async () => {
|
|
68
|
+
await assert.rejects(cursor.emit({ preset: 'loose', projectRoot: tmpRoot }), /invalid preset/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('install writes .mdc files to .cursor/rules/', async () => {
|
|
72
|
+
await seedCursor(tmpRoot);
|
|
73
|
+
const result = await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
|
|
74
|
+
assert.strictEqual(result.written, true);
|
|
75
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
76
|
+
const files = await readdir(rulesDir);
|
|
77
|
+
assert.ok(files.includes('rune-preflight.mdc'));
|
|
78
|
+
assert.ok(files.includes('rune-sentinel.mdc'));
|
|
79
|
+
assert.ok(files.includes('rune-dependency-doctor.mdc'));
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('idempotent re-install produces identical content', async () => {
|
|
83
|
+
await seedCursor(tmpRoot);
|
|
84
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
|
|
85
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
86
|
+
const first = await readFile(path.join(rulesDir, 'rune-preflight.mdc'), 'utf-8');
|
|
87
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
|
|
88
|
+
const second = await readFile(path.join(rulesDir, 'rune-preflight.mdc'), 'utf-8');
|
|
89
|
+
assert.strictEqual(first, second);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('uninstall removes only Rune-managed .mdc files', async () => {
|
|
93
|
+
await seedCursor(tmpRoot);
|
|
94
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
95
|
+
await mkdir(rulesDir, { recursive: true });
|
|
96
|
+
await writeFile(path.join(rulesDir, 'user-custom.mdc'), '---\ndescription: user\n---\n\n# User rule\n', 'utf-8');
|
|
97
|
+
|
|
98
|
+
await installHooks(tmpRoot, { preset: 'gentle', platform: 'cursor' });
|
|
99
|
+
await uninstallHooks(tmpRoot, { platform: 'cursor' });
|
|
100
|
+
|
|
101
|
+
const remaining = await readdir(rulesDir);
|
|
102
|
+
assert.ok(remaining.includes('user-custom.mdc'), 'user rule must survive');
|
|
103
|
+
assert.ok(!remaining.some((f) => f.startsWith('rune-')), 'no rune-* files should remain');
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('uninstall skips files without rune-managed signature', async () => {
|
|
107
|
+
await seedCursor(tmpRoot);
|
|
108
|
+
const rulesDir = path.join(tmpRoot, RULES_DIR);
|
|
109
|
+
await mkdir(rulesDir, { recursive: true });
|
|
110
|
+
// A file that happens to start with "rune-" but is NOT managed by Rune
|
|
111
|
+
await writeFile(
|
|
112
|
+
path.join(rulesDir, 'rune-fake.mdc'),
|
|
113
|
+
'---\ndescription: user imposter\n---\n\nnot ours\n',
|
|
114
|
+
'utf-8',
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
await uninstallHooks(tmpRoot, { platform: 'cursor' });
|
|
118
|
+
assert.ok(existsSync(path.join(rulesDir, 'rune-fake.mdc')), 'non-managed rune-* file must survive');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test('status reports installed preset after install', async () => {
|
|
122
|
+
await seedCursor(tmpRoot);
|
|
123
|
+
await installHooks(tmpRoot, { preset: 'strict', platform: 'cursor' });
|
|
124
|
+
const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'cursor' });
|
|
125
|
+
const r = result.results.find((x) => x.platform === 'cursor');
|
|
126
|
+
assert.strictEqual(r.installed, true);
|
|
127
|
+
assert.strictEqual(r.preset, 'strict');
|
|
128
|
+
assert.ok(r.wired.includes('preflight'));
|
|
129
|
+
assert.ok(r.wired.includes('sentinel'));
|
|
130
|
+
assert.ok(r.wired.includes('dependency-doctor'));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('status reports not installed when .cursor missing', async () => {
|
|
134
|
+
const result = await hookStatus(tmpRoot, RUNE_ROOT, { platform: 'cursor' });
|
|
135
|
+
const r = result.results.find((x) => x.platform === 'cursor');
|
|
136
|
+
assert.strictEqual(r.installed, false);
|
|
137
|
+
assert.ok(r.missing.includes('preflight'));
|
|
138
|
+
});
|
|
139
|
+
});
|