@plainconceptsplatform/workflows 0.6.1 → 0.16.2
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/README.md +107 -88
- package/dist/catalog-installation.d.ts +39 -2
- package/dist/catalog-installation.js +172 -109
- package/dist/index.js +128 -73
- package/dist/package-baseline.d.ts +25 -0
- package/dist/package-baseline.js +138 -0
- package/dist/route-processing.d.ts +0 -2
- package/dist/route-processing.js +22 -91
- package/dist/stack-defaults.js +18 -12
- package/dist/tui.js +27 -43
- package/dist/worker-env.d.ts +46 -0
- package/dist/worker-env.js +179 -0
- package/dist/workflow-catalog.d.ts +4 -2
- package/dist/workflow-catalog.js +5 -3
- package/loops/actions/add-issue-labels/action.yml +20 -0
- package/loops/actions/audit-close/action.yml +180 -128
- package/loops/actions/classify-route/classify-route.sh +8 -2
- package/loops/actions/housekeeping/action.yml +251 -0
- package/loops/actions/merge-agent-pr/action.yml +13 -0
- package/loops/actions/report-workflow-errors/action.yml +414 -0
- package/loops/actions/validate-merge-gate-output/validate-merge-gate-output.sh +13 -1
- package/loops/actions/validate-triage-output/action.yml +1 -1
- package/loops/actions/validate-triage-output/validate-triage-output.sh +9 -5
- package/loops/actions/verify-composite-actions/verify-composite-actions.sh +53 -0
- package/loops/actions/verify-route-matrix/verify-route-matrix.sh +911 -170
- package/loops/templates/agentics/agentics-error-report.yml +98 -0
- package/loops/templates/opencode/opencode.ci.json +1 -1
- package/loops/workflows/agent-apply-review.md +438 -469
- package/loops/workflows/agent-audit.md +200 -213
- package/loops/workflows/agent-implement.md +591 -640
- package/loops/workflows/agent-merge-gate.md +826 -844
- package/loops/workflows/agent-refine.md +590 -633
- package/loops/workflows/agent-release.md +244 -258
- package/loops/workflows/agent-triage.md +476 -447
- package/loops/workflows/authorize-bot-work.yml +26 -6
- package/loops/workflows/shared/platform-defaults.md +18 -1
- package/loops/workflows/work-router.yml +1185 -1038
- package/package.json +9 -8
- package/dist/action-validation.test.d.ts +0 -1
- package/dist/action-validation.test.js +0 -87
- package/dist/catalog-installation.test.d.ts +0 -1
- package/dist/catalog-installation.test.js +0 -485
- package/dist/catalog-listing.test.d.ts +0 -1
- package/dist/catalog-listing.test.js +0 -150
- package/dist/index.test.d.ts +0 -1
- package/dist/index.test.js +0 -273
- package/dist/repository-inspection.test.d.ts +0 -1
- package/dist/repository-inspection.test.js +0 -77
- package/dist/route-processing.test.d.ts +0 -1
- package/dist/route-processing.test.js +0 -283
- package/dist/stack-defaults.test.d.ts +0 -1
- package/dist/stack-defaults.test.js +0 -266
- package/dist/tui.test.d.ts +0 -1
- package/dist/tui.test.js +0 -249
- package/dist/workflow-catalog.test.d.ts +0 -1
- package/dist/workflow-catalog.test.js +0 -29
- package/loops/actions/stale-recovery/action.yml +0 -288
- package/loops/actions/update-changelog/action.yml +0 -113
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { addRouteExclusion, excludedWorkerFiles, processRoutes, stripRouteFromClassifier, stripRouteFromRouter, } from "./route-processing.js";
|
|
3
|
-
import { routeNames } from "./workflow-catalog.js";
|
|
4
|
-
const ROUTER_YAML = `# header
|
|
5
|
-
name: "All Work Router"
|
|
6
|
-
|
|
7
|
-
on:
|
|
8
|
-
schedule:
|
|
9
|
-
- cron: "17 1 * * 1"
|
|
10
|
-
- cron: "43 3 * * *"
|
|
11
|
-
- cron: "0 6 * * *"
|
|
12
|
-
- cron: "*/5 * * * *"
|
|
13
|
-
- cron: "0 */2 * * *"
|
|
14
|
-
|
|
15
|
-
workflow_dispatch:
|
|
16
|
-
inputs:
|
|
17
|
-
operation:
|
|
18
|
-
description: "Operation to run"
|
|
19
|
-
required: true
|
|
20
|
-
type: choice
|
|
21
|
-
options:
|
|
22
|
-
- refine
|
|
23
|
-
- implement
|
|
24
|
-
- triage
|
|
25
|
-
- apply-review
|
|
26
|
-
- merge-gate
|
|
27
|
-
- audit
|
|
28
|
-
- audit-close
|
|
29
|
-
- cleanup-artifacts
|
|
30
|
-
- reconcile-bot-pr-runs
|
|
31
|
-
- stale-recovery
|
|
32
|
-
- validate
|
|
33
|
-
|
|
34
|
-
jobs:
|
|
35
|
-
call-refine:
|
|
36
|
-
needs: [classify, authorize]
|
|
37
|
-
if: needs.classify.outputs.route == 'refine' && needs.authorize.outputs.trusted == 'true'
|
|
38
|
-
uses: ./.github/workflows/agent-refine.lock.yml
|
|
39
|
-
secrets:
|
|
40
|
-
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
41
|
-
|
|
42
|
-
call-implement:
|
|
43
|
-
needs: [classify, authorize]
|
|
44
|
-
if: needs.classify.outputs.route == 'implement' && needs.authorize.outputs.trusted == 'true'
|
|
45
|
-
uses: ./.github/workflows/agent-implement.lock.yml
|
|
46
|
-
secrets:
|
|
47
|
-
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
48
|
-
|
|
49
|
-
call-triage:
|
|
50
|
-
needs: [classify, authorize]
|
|
51
|
-
if: needs.classify.outputs.route == 'triage' && needs.authorize.outputs.is_outside_collaborator == 'true'
|
|
52
|
-
uses: ./.github/workflows/agent-triage.lock.yml
|
|
53
|
-
secrets:
|
|
54
|
-
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
55
|
-
|
|
56
|
-
call-audit:
|
|
57
|
-
needs: classify
|
|
58
|
-
if: needs.classify.outputs.route == 'audit'
|
|
59
|
-
uses: ./.github/workflows/agent-audit.lock.yml
|
|
60
|
-
secrets:
|
|
61
|
-
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
62
|
-
|
|
63
|
-
audit-close:
|
|
64
|
-
needs: classify
|
|
65
|
-
if: needs.classify.outputs.route == 'audit-close'
|
|
66
|
-
runs-on: ubuntu-latest
|
|
67
|
-
`;
|
|
68
|
-
const CLASSIFIER_SH = `#!/usr/bin/env bash
|
|
69
|
-
set -euo pipefail
|
|
70
|
-
|
|
71
|
-
readonly AUDIT_CRON="17 1 * * 1"
|
|
72
|
-
readonly AUDIT_CLOSE_CRON="43 3 * * *"
|
|
73
|
-
readonly CLEANUP_ARTIFACTS_CRON="0 6 * * *"
|
|
74
|
-
readonly RECONCILE_BOT_PR_RUNS_CRON="*/5 * * * *"
|
|
75
|
-
readonly STALE_RECOVERY_CRON="0 */2 * * *"
|
|
76
|
-
|
|
77
|
-
classify_route() {
|
|
78
|
-
local route="none" error=""
|
|
79
|
-
|
|
80
|
-
case "\${EVENT:-}" in
|
|
81
|
-
schedule)
|
|
82
|
-
trigger_kind="scheduled"
|
|
83
|
-
case "\${SCHEDULE:-}" in
|
|
84
|
-
"\$AUDIT_CRON") route="audit" ;;
|
|
85
|
-
"\$AUDIT_CLOSE_CRON") route="audit-close" ;;
|
|
86
|
-
"\$CLEANUP_ARTIFACTS_CRON") route="cleanup-artifacts" ;;
|
|
87
|
-
"\$RECONCILE_BOT_PR_RUNS_CRON") route="reconcile-bot-pr-runs" ;;
|
|
88
|
-
"\$STALE_RECOVERY_CRON") route="stale-recovery" ;;
|
|
89
|
-
*) error="no route for cron '\${SCHEDULE:-}'" ;;
|
|
90
|
-
esac
|
|
91
|
-
;;
|
|
92
|
-
|
|
93
|
-
workflow_dispatch)
|
|
94
|
-
trigger_kind="manual"
|
|
95
|
-
case "\${OPERATION:-}" in
|
|
96
|
-
refine | implement)
|
|
97
|
-
route="\${OPERATION}"
|
|
98
|
-
;;
|
|
99
|
-
apply-review)
|
|
100
|
-
route="apply-review"
|
|
101
|
-
;;
|
|
102
|
-
merge-gate)
|
|
103
|
-
route="merge-gate"
|
|
104
|
-
;;
|
|
105
|
-
audit)
|
|
106
|
-
route="\${OPERATION}"
|
|
107
|
-
trigger_kind="\${INPUT_TRIGGER_KIND:-manual}"
|
|
108
|
-
;;
|
|
109
|
-
audit-close | cleanup-artifacts | reconcile-bot-pr-runs | stale-recovery | validate)
|
|
110
|
-
route="\${OPERATION}"
|
|
111
|
-
;;
|
|
112
|
-
*)
|
|
113
|
-
error="unknown operation '\${OPERATION:-}'"
|
|
114
|
-
;;
|
|
115
|
-
esac
|
|
116
|
-
;;
|
|
117
|
-
esac
|
|
118
|
-
|
|
119
|
-
cat <<EOF
|
|
120
|
-
route=\${route}
|
|
121
|
-
error=\${error}
|
|
122
|
-
EOF
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if [ "\${BASH_SOURCE[0]}" = "\$0" ]; then
|
|
126
|
-
classify_route
|
|
127
|
-
fi
|
|
128
|
-
`;
|
|
129
|
-
const MATRIX_SH = `#!/usr/bin/env bash
|
|
130
|
-
set -euo pipefail
|
|
131
|
-
|
|
132
|
-
echo "── Router wiring ─────────────────────────────────────────────────────────"
|
|
133
|
-
for route in refine implement triage apply-review merge-gate audit bot-approve \\
|
|
134
|
-
audit-close cleanup-artifacts reconcile-bot-pr-runs stale-recovery validate; do
|
|
135
|
-
if grep -q "route == '\${route}'" "\$ROUTER_YML"; then
|
|
136
|
-
PASS=\$((PASS + 1))
|
|
137
|
-
else
|
|
138
|
-
FAIL=\$((FAIL + 1))
|
|
139
|
-
echo "FAIL: work-router.yml has no job for route '\${route}'" >&2
|
|
140
|
-
fi
|
|
141
|
-
done
|
|
142
|
-
|
|
143
|
-
while read -r operation; do
|
|
144
|
-
if grep -q "route == '\${operation}'" "\$ROUTER_YML"; then
|
|
145
|
-
PASS=\$((PASS + 1))
|
|
146
|
-
else
|
|
147
|
-
FAIL=\$((FAIL + 1))
|
|
148
|
-
echo "FAIL: dispatch operation '\${operation}' has no job in work-router.yml" >&2
|
|
149
|
-
fi
|
|
150
|
-
done < <(sed -n '/^ operation:/,/^ issue-number:/p' "\$ROUTER_YML" | sed -n 's/^ - //p')
|
|
151
|
-
|
|
152
|
-
echo
|
|
153
|
-
if [ "\$FAIL" -eq 0 ]; then
|
|
154
|
-
echo "Route matrix: \${PASS} passed"
|
|
155
|
-
else
|
|
156
|
-
echo "Route matrix: \${PASS} passed, \${FAIL} FAILED" >&2
|
|
157
|
-
fi
|
|
158
|
-
|
|
159
|
-
exit \$((FAIL > 0))
|
|
160
|
-
`;
|
|
161
|
-
describe("stripRouteFromRouter", () => {
|
|
162
|
-
it("removes the audit cron entry", () => {
|
|
163
|
-
const result = stripRouteFromRouter(ROUTER_YAML, "audit");
|
|
164
|
-
expect(result).not.toContain('cron: "17 1 * * 1"');
|
|
165
|
-
expect(result).toContain('cron: "43 3 * * *"');
|
|
166
|
-
});
|
|
167
|
-
it("removes the call-audit job block", () => {
|
|
168
|
-
const result = stripRouteFromRouter(ROUTER_YAML, "audit");
|
|
169
|
-
expect(result).not.toContain("call-audit");
|
|
170
|
-
expect(result).toContain("call-refine");
|
|
171
|
-
});
|
|
172
|
-
it("preserves the audit-close job when removing audit", () => {
|
|
173
|
-
const result = stripRouteFromRouter(ROUTER_YAML, "audit");
|
|
174
|
-
expect(result).toContain("audit-close");
|
|
175
|
-
});
|
|
176
|
-
it("does not modify the yaml when stripping a route that has no cron", () => {
|
|
177
|
-
const yamlWithoutCron = ROUTER_YAML.replace(/ - cron: "17 1 \* \* 1"\n/, "");
|
|
178
|
-
const result = stripRouteFromRouter(yamlWithoutCron, "audit");
|
|
179
|
-
expect(result).not.toContain("call-audit");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
describe("stripRouteFromClassifier", () => {
|
|
183
|
-
it("removes the AUDIT_CRON constant", () => {
|
|
184
|
-
const result = stripRouteFromClassifier(CLASSIFIER_SH, "audit");
|
|
185
|
-
expect(result).not.toContain('readonly AUDIT_CRON');
|
|
186
|
-
expect(result).toContain('readonly AUDIT_CLOSE_CRON');
|
|
187
|
-
});
|
|
188
|
-
it("removes the audit schedule case", () => {
|
|
189
|
-
const result = stripRouteFromClassifier(CLASSIFIER_SH, "audit");
|
|
190
|
-
expect(result).not.toContain('"$AUDIT_CRON") route="audit"');
|
|
191
|
-
expect(result).toContain('"$AUDIT_CLOSE_CRON") route="audit-close"');
|
|
192
|
-
});
|
|
193
|
-
});
|
|
194
|
-
describe("addRouteExclusion", () => {
|
|
195
|
-
it("adds an exclusion assertion for the route", () => {
|
|
196
|
-
const result = addRouteExclusion(MATRIX_SH, "audit");
|
|
197
|
-
expect(result).toContain("excluded route 'audit'");
|
|
198
|
-
expect(result).toContain("audit correctly excluded from work-router.yml");
|
|
199
|
-
});
|
|
200
|
-
it("does not add the exclusion twice", () => {
|
|
201
|
-
const once = addRouteExclusion(MATRIX_SH, "audit");
|
|
202
|
-
const twice = addRouteExclusion(once, "audit");
|
|
203
|
-
const matchCount = (twice.match(/excluded route 'audit'/g) ?? []).length;
|
|
204
|
-
expect(matchCount).toBe(1);
|
|
205
|
-
});
|
|
206
|
-
});
|
|
207
|
-
describe("processRoutes", () => {
|
|
208
|
-
it("returns the same map when all routes are selected", () => {
|
|
209
|
-
const files = new Map([
|
|
210
|
-
["work-router.yml", ROUTER_YAML],
|
|
211
|
-
["classify-route.sh", CLASSIFIER_SH],
|
|
212
|
-
["verify-route-matrix.sh", MATRIX_SH],
|
|
213
|
-
]);
|
|
214
|
-
const result = processRoutes(files, [...routeNames]);
|
|
215
|
-
expect(result).toBe(files);
|
|
216
|
-
});
|
|
217
|
-
it("strips every worker route when no routes are selected", () => {
|
|
218
|
-
const files = new Map([
|
|
219
|
-
[".github/workflows/work-router.yml", ROUTER_YAML],
|
|
220
|
-
[".github/actions/classify-route/classify-route.sh", CLASSIFIER_SH],
|
|
221
|
-
[".github/actions/verify-route-matrix/verify-route-matrix.sh", MATRIX_SH],
|
|
222
|
-
]);
|
|
223
|
-
const result = processRoutes(files, []);
|
|
224
|
-
const router = result.get(".github/workflows/work-router.yml");
|
|
225
|
-
expect(router).not.toContain("call-audit");
|
|
226
|
-
expect(router).not.toContain("- refine");
|
|
227
|
-
const classifier = result.get(".github/actions/classify-route/classify-route.sh");
|
|
228
|
-
expect(classifier).toBe(CLASSIFIER_SH);
|
|
229
|
-
const matrix = result.get(".github/actions/verify-route-matrix/verify-route-matrix.sh");
|
|
230
|
-
expect(matrix).toContain("Route matrix: selected routes valid");
|
|
231
|
-
expect(matrix).toContain("for route in refine implement triage apply-review merge-gate audit");
|
|
232
|
-
});
|
|
233
|
-
it("strips audit from all three files when unselected", () => {
|
|
234
|
-
const files = new Map([
|
|
235
|
-
["work-router.yml", ROUTER_YAML],
|
|
236
|
-
["classify-route.sh", CLASSIFIER_SH],
|
|
237
|
-
["verify-route-matrix.sh", MATRIX_SH],
|
|
238
|
-
]);
|
|
239
|
-
const selectedRoutes = routeNames.filter((r) => r !== "audit");
|
|
240
|
-
const result = processRoutes(files, selectedRoutes);
|
|
241
|
-
const router = result.get("work-router.yml");
|
|
242
|
-
expect(router).not.toContain("call-audit");
|
|
243
|
-
expect(router).not.toContain('cron: "17 1 * * 1"');
|
|
244
|
-
const classifier = result.get("classify-route.sh");
|
|
245
|
-
expect(classifier).toBe(CLASSIFIER_SH);
|
|
246
|
-
});
|
|
247
|
-
it("processes multiple excluded routes at once", () => {
|
|
248
|
-
const files = new Map([
|
|
249
|
-
["work-router.yml", ROUTER_YAML],
|
|
250
|
-
["classify-route.sh", CLASSIFIER_SH],
|
|
251
|
-
["verify-route-matrix.sh", MATRIX_SH],
|
|
252
|
-
]);
|
|
253
|
-
const selectedRoutes = ["refine", "implement"];
|
|
254
|
-
const result = processRoutes(files, selectedRoutes);
|
|
255
|
-
const router = result.get("work-router.yml");
|
|
256
|
-
expect(router).toContain("call-refine");
|
|
257
|
-
expect(router).toContain("call-implement");
|
|
258
|
-
expect(router).not.toContain("call-audit");
|
|
259
|
-
});
|
|
260
|
-
});
|
|
261
|
-
describe("excludedWorkerFiles", () => {
|
|
262
|
-
it("returns worker files for unselected routes", () => {
|
|
263
|
-
const selectedRoutes = ["refine", "implement"];
|
|
264
|
-
const excluded = excludedWorkerFiles(selectedRoutes);
|
|
265
|
-
expect(excluded.has("agent-refine.md")).toBe(false);
|
|
266
|
-
expect(excluded.has("agent-implement.md")).toBe(false);
|
|
267
|
-
expect(excluded.has("agent-audit.md")).toBe(true);
|
|
268
|
-
});
|
|
269
|
-
it("returns an empty set when all routes are selected", () => {
|
|
270
|
-
const excluded = excludedWorkerFiles([...routeNames]);
|
|
271
|
-
expect(excluded.size).toBe(0);
|
|
272
|
-
});
|
|
273
|
-
it("returns all worker files when no routes are selected", () => {
|
|
274
|
-
const excluded = excludedWorkerFiles([]);
|
|
275
|
-
expect(excluded.size).toBe(6);
|
|
276
|
-
expect(excluded.has("agent-refine.md")).toBe(true);
|
|
277
|
-
expect(excluded.has("agent-implement.md")).toBe(true);
|
|
278
|
-
expect(excluded.has("agent-triage.md")).toBe(true);
|
|
279
|
-
expect(excluded.has("agent-apply-review.md")).toBe(true);
|
|
280
|
-
expect(excluded.has("agent-merge-gate.md")).toBe(true);
|
|
281
|
-
expect(excluded.has("agent-audit.md")).toBe(true);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,266 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { generateOpencodeCi, generateOpencodeConfig, generateStackDefaults, injectStackEnv, } from "./stack-defaults.js";
|
|
3
|
-
function makeInspection(overrides = {}) {
|
|
4
|
-
return {
|
|
5
|
-
repositoryPath: "/repo",
|
|
6
|
-
existingAgentWorkflows: [],
|
|
7
|
-
stackHints: {
|
|
8
|
-
packageJson: false,
|
|
9
|
-
pnpmLockfile: false,
|
|
10
|
-
solutionFiles: [],
|
|
11
|
-
openSpec: false,
|
|
12
|
-
...overrides,
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
}
|
|
16
|
-
describe("generateStackDefaults", () => {
|
|
17
|
-
it("returns .NET verify commands when .slnx is found", () => {
|
|
18
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
19
|
-
solutionFiles: ["app.slnx"],
|
|
20
|
-
}));
|
|
21
|
-
expect(defaults.verifyCommands).toBe("dotnet restore && dotnet build -c Release --no-restore && dotnet test");
|
|
22
|
-
expect(defaults.hasDotnet).toBe(true);
|
|
23
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
24
|
-
});
|
|
25
|
-
it("returns pnpm verify when pnpm-lock.yaml is found without .slnx", () => {
|
|
26
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
27
|
-
pnpmLockfile: true,
|
|
28
|
-
}));
|
|
29
|
-
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
30
|
-
expect(defaults.hasDotnet).toBe(false);
|
|
31
|
-
expect(defaults.hasNodeOnly).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
it("returns both .NET and Web verify commands for a full-stack repo", () => {
|
|
34
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
35
|
-
solutionFiles: ["app.slnx"],
|
|
36
|
-
pnpmLockfile: true,
|
|
37
|
-
}));
|
|
38
|
-
expect(defaults.verifyCommands).toContain(".NET");
|
|
39
|
-
expect(defaults.verifyCommands).toContain("dotnet restore");
|
|
40
|
-
expect(defaults.verifyCommands).toContain("dotnet build -c Release");
|
|
41
|
-
expect(defaults.verifyCommands).toContain("dotnet test");
|
|
42
|
-
expect(defaults.verifyCommands).toContain("Web");
|
|
43
|
-
expect(defaults.verifyCommands).toContain("pnpm lint");
|
|
44
|
-
expect(defaults.verifyCommands).toContain("pnpm test");
|
|
45
|
-
expect(defaults.verifyCommands).toContain("pnpm build");
|
|
46
|
-
expect(defaults.hasDotnet).toBe(true);
|
|
47
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
it("returns pnpm verify when neither .slnx nor pnpm-lock.yaml is present", () => {
|
|
50
|
-
const defaults = generateStackDefaults(makeInspection());
|
|
51
|
-
expect(defaults.verifyCommands).toBe("pnpm verify");
|
|
52
|
-
expect(defaults.hasDotnet).toBe(false);
|
|
53
|
-
expect(defaults.hasNodeOnly).toBe(false);
|
|
54
|
-
});
|
|
55
|
-
it("includes a repo rules base string with architecture context", () => {
|
|
56
|
-
const dotnetDefaults = generateStackDefaults(makeInspection({
|
|
57
|
-
solutionFiles: ["app.slnx"],
|
|
58
|
-
}));
|
|
59
|
-
expect(dotnetDefaults.repoRulesBase).toContain("Clean Architecture");
|
|
60
|
-
const nodeDefaults = generateStackDefaults(makeInspection({
|
|
61
|
-
pnpmLockfile: true,
|
|
62
|
-
}));
|
|
63
|
-
expect(nodeDefaults.repoRulesBase).toContain("Node.js");
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
describe("injectStackEnv", () => {
|
|
67
|
-
it("injects VERIFY_COMMANDS into a worker that has an env block", () => {
|
|
68
|
-
const content = `---
|
|
69
|
-
env:
|
|
70
|
-
REPO_RULES: "some rules"
|
|
71
|
-
description: test
|
|
72
|
-
---`;
|
|
73
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
74
|
-
solutionFiles: ["app.slnx"],
|
|
75
|
-
}));
|
|
76
|
-
const result = injectStackEnv(content, defaults);
|
|
77
|
-
expect(result).toContain('VERIFY_COMMANDS: "dotnet restore && dotnet build -c Release --no-restore && dotnet test"');
|
|
78
|
-
});
|
|
79
|
-
it("injects pnpm verification when no .slnx is present", () => {
|
|
80
|
-
const content = `---
|
|
81
|
-
env:
|
|
82
|
-
REPO_RULES: "some rules"
|
|
83
|
-
---`;
|
|
84
|
-
const defaults = generateStackDefaults(makeInspection({
|
|
85
|
-
pnpmLockfile: true,
|
|
86
|
-
}));
|
|
87
|
-
const result = injectStackEnv(content, defaults);
|
|
88
|
-
expect(result).toContain('VERIFY_COMMANDS: "pnpm verify"');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
const OPENCODE_CI_MD = `---
|
|
92
|
-
env:
|
|
93
|
-
AGENTMEMORY_VERSION: "0.9.28"
|
|
94
|
-
CODEGRAPH_VERSION: "1.5.0"
|
|
95
|
-
description: Shared CI setup.
|
|
96
|
-
|
|
97
|
-
pre-agent-steps:
|
|
98
|
-
- name: Install RTK
|
|
99
|
-
run: |
|
|
100
|
-
rtk --version
|
|
101
|
-
rtk init -g --opencode --auto-patch
|
|
102
|
-
- name: Install opencode plugin dependencies
|
|
103
|
-
run: |
|
|
104
|
-
set -euo pipefail
|
|
105
|
-
if [ ! -f .opencode/package.json ]; then
|
|
106
|
-
echo "No .opencode/package.json, nothing to install"
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
- name: Install workspace dependencies
|
|
110
|
-
run: pnpm install --frozen-lockfile
|
|
111
|
-
- name: Merge the CI-only OpenCode provider into opencode.jsonc
|
|
112
|
-
run: |
|
|
113
|
-
jq -e . "$FRAGMENT" > /dev/null
|
|
114
|
-
---`;
|
|
115
|
-
describe("generateOpencodeCi", () => {
|
|
116
|
-
it("adds NuGet cache and restores detected solution when .slnx is found", () => {
|
|
117
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
118
|
-
solutionFiles: ["apps/api/Contoso.slnx"],
|
|
119
|
-
}));
|
|
120
|
-
expect(result).toContain("Cache NuGet packages");
|
|
121
|
-
expect(result).toContain("Restore .NET dependencies");
|
|
122
|
-
expect(result).toContain("dotnet restore apps/api/Contoso.slnx");
|
|
123
|
-
expect(result).toContain("\n - name: Restore .NET dependencies\n");
|
|
124
|
-
expect(result).not.toContain("\n - name: Restore .NET dependencies\n");
|
|
125
|
-
});
|
|
126
|
-
it("uses POSIX separators for .NET restore commands", () => {
|
|
127
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
128
|
-
solutionFiles: ["apps\\api\\Contoso.slnx"],
|
|
129
|
-
}));
|
|
130
|
-
expect(result).toContain("dotnet restore apps/api/Contoso.slnx");
|
|
131
|
-
expect(result).not.toContain("dotnet restore apps\\api\\Contoso.slnx");
|
|
132
|
-
});
|
|
133
|
-
it("adds the pinned OpenSpec CLI install step when openspec/ directory exists", () => {
|
|
134
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
135
|
-
openSpec: true,
|
|
136
|
-
}));
|
|
137
|
-
expect(result).toContain("Install OpenSpec CLI");
|
|
138
|
-
expect(result).toContain("@fission-ai/openspec@1.8.0");
|
|
139
|
-
});
|
|
140
|
-
it("adds both NuGet and OpenSpec steps when both are detected", () => {
|
|
141
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
142
|
-
solutionFiles: ["app.slnx"],
|
|
143
|
-
openSpec: true,
|
|
144
|
-
}));
|
|
145
|
-
expect(result).toContain("Cache NuGet packages");
|
|
146
|
-
expect(result).toContain("Install OpenSpec CLI");
|
|
147
|
-
});
|
|
148
|
-
it("does not add NuGet steps when no .slnx is present", () => {
|
|
149
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
150
|
-
pnpmLockfile: true,
|
|
151
|
-
}));
|
|
152
|
-
expect(result).not.toContain("Cache NuGet packages");
|
|
153
|
-
expect(result).not.toContain("Restore .NET dependencies");
|
|
154
|
-
});
|
|
155
|
-
it("preserves the original merge step", () => {
|
|
156
|
-
const result = generateOpencodeCi(OPENCODE_CI_MD, makeInspection({
|
|
157
|
-
solutionFiles: ["app.slnx"],
|
|
158
|
-
openSpec: true,
|
|
159
|
-
}));
|
|
160
|
-
expect(result).toContain("Merge the CI-only OpenCode provider");
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
const OPENCODE_CI_JSON = `{
|
|
164
|
-
"$schema": "https://opencode.ai/config.json",
|
|
165
|
-
"model": "plainconcepts/glm-5-3",
|
|
166
|
-
"plugin": [],
|
|
167
|
-
"default_agent": "ci-workflow-agent",
|
|
168
|
-
"agent": {
|
|
169
|
-
"ci-workflow-agent": {
|
|
170
|
-
"description": "Executes GitHub Agentic Workflow tasks in CI.",
|
|
171
|
-
"mode": "primary",
|
|
172
|
-
"prompt": "You execute the GitHub Agentic Workflow task in the user prompt."
|
|
173
|
-
}
|
|
174
|
-
},
|
|
175
|
-
"permission": {
|
|
176
|
-
"read": "allow"
|
|
177
|
-
},
|
|
178
|
-
"lsp": {
|
|
179
|
-
"csharp": {
|
|
180
|
-
"disabled": true
|
|
181
|
-
},
|
|
182
|
-
"fsharp": {
|
|
183
|
-
"disabled": true
|
|
184
|
-
},
|
|
185
|
-
"razor": {
|
|
186
|
-
"disabled": true
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
"provider": {
|
|
190
|
-
"plainconcepts": {
|
|
191
|
-
"api": "http://172.30.0.30:10000",
|
|
192
|
-
"options": {
|
|
193
|
-
"apiKey": "awf-openai-proxy"
|
|
194
|
-
},
|
|
195
|
-
"models": {
|
|
196
|
-
"glm-5-3": {
|
|
197
|
-
"name": "GLM 5.3"
|
|
198
|
-
},
|
|
199
|
-
"glm-5-2": {
|
|
200
|
-
"name": "GLM 5.2"
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
`;
|
|
207
|
-
describe("generateOpencodeConfig", () => {
|
|
208
|
-
it("keeps LSP section when .slnx is present", () => {
|
|
209
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
210
|
-
solutionFiles: ["app.slnx"],
|
|
211
|
-
}));
|
|
212
|
-
const parsed = JSON.parse(result);
|
|
213
|
-
expect(parsed.lsp).toBeDefined();
|
|
214
|
-
expect(parsed.lsp.csharp.disabled).toBe(true);
|
|
215
|
-
expect(parsed.lsp.fsharp.disabled).toBe(true);
|
|
216
|
-
expect(parsed.lsp.razor.disabled).toBe(true);
|
|
217
|
-
});
|
|
218
|
-
it("removes LSP section when no .slnx is present", () => {
|
|
219
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
220
|
-
pnpmLockfile: true,
|
|
221
|
-
}));
|
|
222
|
-
const parsed = JSON.parse(result);
|
|
223
|
-
expect(parsed.lsp).toBeUndefined();
|
|
224
|
-
});
|
|
225
|
-
it("adds .NET guardrails to agent prompt when .slnx is present and prompt lacks them", () => {
|
|
226
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
227
|
-
solutionFiles: ["app.slnx"],
|
|
228
|
-
}));
|
|
229
|
-
const parsed = JSON.parse(result);
|
|
230
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain(".NET guardrails");
|
|
231
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Clean Architecture");
|
|
232
|
-
});
|
|
233
|
-
it("adds Node/React rules to agent prompt when no .slnx", () => {
|
|
234
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
235
|
-
pnpmLockfile: true,
|
|
236
|
-
}));
|
|
237
|
-
const parsed = JSON.parse(result);
|
|
238
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Node/React rules");
|
|
239
|
-
expect(parsed.agent["ci-workflow-agent"].prompt).toContain("Feature-Sliced Design");
|
|
240
|
-
});
|
|
241
|
-
it("preserves both models in the provider", () => {
|
|
242
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
243
|
-
solutionFiles: ["app.slnx"],
|
|
244
|
-
}));
|
|
245
|
-
const parsed = JSON.parse(result);
|
|
246
|
-
expect(parsed.provider.plainconcepts.models["glm-5-3"]).toBeDefined();
|
|
247
|
-
expect(parsed.provider.plainconcepts.models["glm-5-2"]).toBeDefined();
|
|
248
|
-
});
|
|
249
|
-
it("preserves the plainconcepts provider and its API URL", () => {
|
|
250
|
-
const result = generateOpencodeConfig(OPENCODE_CI_JSON, makeInspection({
|
|
251
|
-
solutionFiles: ["app.slnx"],
|
|
252
|
-
}));
|
|
253
|
-
const parsed = JSON.parse(result);
|
|
254
|
-
expect(parsed.provider.plainconcepts.api).toBe("http://172.30.0.30:10000");
|
|
255
|
-
expect(parsed.provider.plainconcepts.options.apiKey).toBe("awf-openai-proxy");
|
|
256
|
-
});
|
|
257
|
-
it("does not add .NET guardrails twice when already present", () => {
|
|
258
|
-
const withGuardrails = OPENCODE_CI_JSON.replace('"You execute the GitHub Agentic Workflow task in the user prompt."', '"You execute the GitHub Agentic Workflow task in the user prompt.\\n\\n# .NET guardrails\\nFollow Clean Architecture."');
|
|
259
|
-
const result = generateOpencodeConfig(withGuardrails, makeInspection({
|
|
260
|
-
solutionFiles: ["app.slnx"],
|
|
261
|
-
}));
|
|
262
|
-
const parsed = JSON.parse(result);
|
|
263
|
-
const guardrailsCount = (parsed.agent["ci-workflow-agent"].prompt.match(/\.NET guardrails/g) ?? []).length;
|
|
264
|
-
expect(guardrailsCount).toBe(1);
|
|
265
|
-
});
|
|
266
|
-
});
|
package/dist/tui.test.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|