@united-workforce/cli 0.1.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 -0
- package/README.md +221 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts +2 -0
- package/dist/__tests__/adapter-json-roundtrip.test.d.ts.map +1 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js +147 -0
- package/dist/__tests__/adapter-json-roundtrip.test.js.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.js +685 -0
- package/dist/__tests__/config.test.js.map +1 -0
- package/dist/__tests__/current-role.test.d.ts +2 -0
- package/dist/__tests__/current-role.test.d.ts.map +1 -0
- package/dist/__tests__/current-role.test.js +401 -0
- package/dist/__tests__/current-role.test.js.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts +2 -0
- package/dist/__tests__/e2e-mock-agent.test.d.ts.map +1 -0
- package/dist/__tests__/e2e-mock-agent.test.js +401 -0
- package/dist/__tests__/e2e-mock-agent.test.js.map +1 -0
- package/dist/__tests__/include-tag.test.d.ts +2 -0
- package/dist/__tests__/include-tag.test.d.ts.map +1 -0
- package/dist/__tests__/include-tag.test.js +69 -0
- package/dist/__tests__/include-tag.test.js.map +1 -0
- package/dist/__tests__/log.test.d.ts +2 -0
- package/dist/__tests__/log.test.d.ts.map +1 -0
- package/dist/__tests__/log.test.js +161 -0
- package/dist/__tests__/log.test.js.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts +2 -0
- package/dist/__tests__/moderator-evaluate.test.d.ts.map +1 -0
- package/dist/__tests__/moderator-evaluate.test.js +170 -0
- package/dist/__tests__/moderator-evaluate.test.js.map +1 -0
- package/dist/__tests__/preload.d.ts +3 -0
- package/dist/__tests__/preload.d.ts.map +1 -0
- package/dist/__tests__/preload.js +6 -0
- package/dist/__tests__/preload.js.map +1 -0
- package/dist/__tests__/prompt.test.d.ts +2 -0
- package/dist/__tests__/prompt.test.d.ts.map +1 -0
- package/dist/__tests__/prompt.test.js +111 -0
- package/dist/__tests__/prompt.test.js.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts +2 -0
- package/dist/__tests__/resolve-head-hash.test.d.ts.map +1 -0
- package/dist/__tests__/resolve-head-hash.test.js +66 -0
- package/dist/__tests__/resolve-head-hash.test.js.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts +2 -0
- package/dist/__tests__/setup-agent-discovery.test.d.ts.map +1 -0
- package/dist/__tests__/setup-agent-discovery.test.js +119 -0
- package/dist/__tests__/setup-agent-discovery.test.js.map +1 -0
- package/dist/__tests__/setup-complexity.test.d.ts +2 -0
- package/dist/__tests__/setup-complexity.test.d.ts.map +1 -0
- package/dist/__tests__/setup-complexity.test.js +314 -0
- package/dist/__tests__/setup-complexity.test.js.map +1 -0
- package/dist/__tests__/setup-validate.test.d.ts +2 -0
- package/dist/__tests__/setup-validate.test.d.ts.map +1 -0
- package/dist/__tests__/setup-validate.test.js +108 -0
- package/dist/__tests__/setup-validate.test.js.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts +2 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.d.ts.map +1 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js +107 -0
- package/dist/__tests__/solve-issue-tea-worktree.test.js.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts +2 -0
- package/dist/__tests__/spawn-agent-json.test.d.ts.map +1 -0
- package/dist/__tests__/spawn-agent-json.test.js +79 -0
- package/dist/__tests__/spawn-agent-json.test.js.map +1 -0
- package/dist/__tests__/step-read.test.d.ts +2 -0
- package/dist/__tests__/step-read.test.d.ts.map +1 -0
- package/dist/__tests__/step-read.test.js +561 -0
- package/dist/__tests__/step-read.test.js.map +1 -0
- package/dist/__tests__/step-show-json.test.d.ts +2 -0
- package/dist/__tests__/step-show-json.test.d.ts.map +1 -0
- package/dist/__tests__/step-show-json.test.js +311 -0
- package/dist/__tests__/step-show-json.test.js.map +1 -0
- package/dist/__tests__/step-timing.test.d.ts +2 -0
- package/dist/__tests__/step-timing.test.d.ts.map +1 -0
- package/dist/__tests__/step-timing.test.js +345 -0
- package/dist/__tests__/step-timing.test.js.map +1 -0
- package/dist/__tests__/store-global-cas.test.d.ts +2 -0
- package/dist/__tests__/store-global-cas.test.d.ts.map +1 -0
- package/dist/__tests__/store-global-cas.test.js +235 -0
- package/dist/__tests__/store-global-cas.test.js.map +1 -0
- package/dist/__tests__/store-storage-root.test.d.ts +2 -0
- package/dist/__tests__/store-storage-root.test.d.ts.map +1 -0
- package/dist/__tests__/store-storage-root.test.js +43 -0
- package/dist/__tests__/store-storage-root.test.js.map +1 -0
- package/dist/__tests__/store-unified-threads.test.d.ts +2 -0
- package/dist/__tests__/store-unified-threads.test.d.ts.map +1 -0
- package/dist/__tests__/store-unified-threads.test.js +189 -0
- package/dist/__tests__/store-unified-threads.test.js.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts +2 -0
- package/dist/__tests__/thread-cancel-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-cancel-status.test.js +111 -0
- package/dist/__tests__/thread-cancel-status.test.js.map +1 -0
- package/dist/__tests__/thread-list-filters.test.d.ts +2 -0
- package/dist/__tests__/thread-list-filters.test.d.ts.map +1 -0
- package/dist/__tests__/thread-list-filters.test.js +442 -0
- package/dist/__tests__/thread-list-filters.test.js.map +1 -0
- package/dist/__tests__/thread-location.test.d.ts +2 -0
- package/dist/__tests__/thread-location.test.d.ts.map +1 -0
- package/dist/__tests__/thread-location.test.js +159 -0
- package/dist/__tests__/thread-location.test.js.map +1 -0
- package/dist/__tests__/thread-read-quota.test.d.ts +2 -0
- package/dist/__tests__/thread-read-quota.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-quota.test.js +546 -0
- package/dist/__tests__/thread-read-quota.test.js.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts +2 -0
- package/dist/__tests__/thread-read-xml-tags.test.d.ts.map +1 -0
- package/dist/__tests__/thread-read-xml-tags.test.js +610 -0
- package/dist/__tests__/thread-read-xml-tags.test.js.map +1 -0
- package/dist/__tests__/thread-resume.test.d.ts +2 -0
- package/dist/__tests__/thread-resume.test.d.ts.map +1 -0
- package/dist/__tests__/thread-resume.test.js +592 -0
- package/dist/__tests__/thread-resume.test.js.map +1 -0
- package/dist/__tests__/thread-show-status.test.d.ts +2 -0
- package/dist/__tests__/thread-show-status.test.d.ts.map +1 -0
- package/dist/__tests__/thread-show-status.test.js +267 -0
- package/dist/__tests__/thread-show-status.test.js.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts +2 -0
- package/dist/__tests__/thread-start-cwd-cli.test.d.ts.map +1 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js +130 -0
- package/dist/__tests__/thread-start-cwd-cli.test.js.map +1 -0
- package/dist/__tests__/thread-step-count.test.d.ts +2 -0
- package/dist/__tests__/thread-step-count.test.d.ts.map +1 -0
- package/dist/__tests__/thread-step-count.test.js +55 -0
- package/dist/__tests__/thread-step-count.test.js.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts +2 -0
- package/dist/__tests__/thread-suspend-step.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspend-step.test.js +155 -0
- package/dist/__tests__/thread-suspend-step.test.js.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts +2 -0
- package/dist/__tests__/thread-suspended-display.test.d.ts.map +1 -0
- package/dist/__tests__/thread-suspended-display.test.js +247 -0
- package/dist/__tests__/thread-suspended-display.test.js.map +1 -0
- package/dist/__tests__/thread-test-helpers.d.ts +4 -0
- package/dist/__tests__/thread-test-helpers.d.ts.map +1 -0
- package/dist/__tests__/thread-test-helpers.js +23 -0
- package/dist/__tests__/thread-test-helpers.js.map +1 -0
- package/dist/__tests__/thread.test.d.ts +2 -0
- package/dist/__tests__/thread.test.d.ts.map +1 -0
- package/dist/__tests__/thread.test.js +883 -0
- package/dist/__tests__/thread.test.js.map +1 -0
- package/dist/__tests__/validate-semantic.test.d.ts +2 -0
- package/dist/__tests__/validate-semantic.test.d.ts.map +1 -0
- package/dist/__tests__/validate-semantic.test.js +408 -0
- package/dist/__tests__/validate-semantic.test.js.map +1 -0
- package/dist/__tests__/workflow-resolution.test.d.ts +2 -0
- package/dist/__tests__/workflow-resolution.test.d.ts.map +1 -0
- package/dist/__tests__/workflow-resolution.test.js +308 -0
- package/dist/__tests__/workflow-resolution.test.js.map +1 -0
- package/dist/background/background.d.ts +38 -0
- package/dist/background/background.d.ts.map +1 -0
- package/dist/background/background.js +123 -0
- package/dist/background/background.js.map +1 -0
- package/dist/background/index.d.ts +3 -0
- package/dist/background/index.d.ts.map +1 -0
- package/dist/background/index.js +2 -0
- package/dist/background/index.js.map +1 -0
- package/dist/background/types.d.ts +9 -0
- package/dist/background/types.d.ts.map +1 -0
- package/dist/background/types.js +2 -0
- package/dist/background/types.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +535 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/config.d.ts +41 -0
- package/dist/commands/config.d.ts.map +1 -0
- package/dist/commands/config.js +252 -0
- package/dist/commands/config.js.map +1 -0
- package/dist/commands/log.d.ts +26 -0
- package/dist/commands/log.d.ts.map +1 -0
- package/dist/commands/log.js +79 -0
- package/dist/commands/log.js.map +1 -0
- package/dist/commands/prompt.d.ts +6 -0
- package/dist/commands/prompt.d.ts.map +1 -0
- package/dist/commands/prompt.js +67 -0
- package/dist/commands/prompt.js.map +1 -0
- package/dist/commands/setup.d.ts +73 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +522 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/shared.d.ts +31 -0
- package/dist/commands/shared.d.ts.map +1 -0
- package/dist/commands/shared.js +154 -0
- package/dist/commands/shared.js.map +1 -0
- package/dist/commands/step.d.ts +18 -0
- package/dist/commands/step.d.ts.map +1 -0
- package/dist/commands/step.js +257 -0
- package/dist/commands/step.js.map +1 -0
- package/dist/commands/thread-time-parser.d.ts +6 -0
- package/dist/commands/thread-time-parser.d.ts.map +1 -0
- package/dist/commands/thread-time-parser.js +22 -0
- package/dist/commands/thread-time-parser.js.map +1 -0
- package/dist/commands/thread.d.ts +38 -0
- package/dist/commands/thread.d.ts.map +1 -0
- package/dist/commands/thread.js +1087 -0
- package/dist/commands/thread.js.map +1 -0
- package/dist/commands/workflow.d.ts +24 -0
- package/dist/commands/workflow.d.ts.map +1 -0
- package/dist/commands/workflow.js +138 -0
- package/dist/commands/workflow.js.map +1 -0
- package/dist/format.d.ts +3 -0
- package/dist/format.d.ts.map +1 -0
- package/dist/format.js +10 -0
- package/dist/format.js.map +1 -0
- package/dist/include.d.ts +12 -0
- package/dist/include.d.ts.map +1 -0
- package/dist/include.js +35 -0
- package/dist/include.js.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts +2 -0
- package/dist/moderator/__tests__/evaluate.test.d.ts.map +1 -0
- package/dist/moderator/__tests__/evaluate.test.js +167 -0
- package/dist/moderator/__tests__/evaluate.test.js.map +1 -0
- package/dist/moderator/evaluate.d.ts +6 -0
- package/dist/moderator/evaluate.d.ts.map +1 -0
- package/dist/moderator/evaluate.js +65 -0
- package/dist/moderator/evaluate.js.map +1 -0
- package/dist/moderator/index.d.ts +4 -0
- package/dist/moderator/index.d.ts.map +1 -0
- package/dist/moderator/index.js +3 -0
- package/dist/moderator/index.js.map +1 -0
- package/dist/moderator/types.d.ts +25 -0
- package/dist/moderator/types.d.ts.map +1 -0
- package/dist/moderator/types.js +4 -0
- package/dist/moderator/types.js.map +1 -0
- package/dist/schemas.d.ts +16 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +17 -0
- package/dist/schemas.js.map +1 -0
- package/dist/store.d.ts +77 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +392 -0
- package/dist/store.js.map +1 -0
- package/dist/validate-semantic.d.ts +7 -0
- package/dist/validate-semantic.d.ts.map +1 -0
- package/dist/validate-semantic.js +263 -0
- package/dist/validate-semantic.js.map +1 -0
- package/dist/validate.d.ts +16 -0
- package/dist/validate.d.ts.map +1 -0
- package/dist/validate.js +115 -0
- package/dist/validate.js.map +1 -0
- package/package.json +44 -0
- package/src/__tests__/adapter-json-roundtrip.test.ts +181 -0
- package/src/__tests__/config.test.ts +740 -0
- package/src/__tests__/current-role.test.ts +438 -0
- package/src/__tests__/e2e-mock-agent.test.ts +498 -0
- package/src/__tests__/fixtures/e2e-completed-resume.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-count.mock.yaml +19 -0
- package/src/__tests__/fixtures/e2e-count.workflow.yaml +45 -0
- package/src/__tests__/fixtures/e2e-linear.mock.yaml +13 -0
- package/src/__tests__/fixtures/e2e-linear.workflow.yaml +32 -0
- package/src/__tests__/fixtures/e2e-loop.mock.yaml +25 -0
- package/src/__tests__/fixtures/e2e-loop.workflow.yaml +36 -0
- package/src/__tests__/fixtures/e2e-mismatch.mock.yaml +16 -0
- package/src/__tests__/fixtures/e2e-mustache.mock.yaml +15 -0
- package/src/__tests__/fixtures/e2e-mustache.workflow.yaml +34 -0
- package/src/__tests__/fixtures/e2e-suspend.mock.yaml +14 -0
- package/src/__tests__/fixtures/e2e-suspend.workflow.yaml +24 -0
- package/src/__tests__/include-tag.test.ts +84 -0
- package/src/__tests__/log.test.ts +181 -0
- package/src/__tests__/moderator-evaluate.test.ts +186 -0
- package/src/__tests__/preload.ts +7 -0
- package/src/__tests__/prompt.test.ts +129 -0
- package/src/__tests__/resolve-head-hash.test.ts +86 -0
- package/src/__tests__/setup-agent-discovery.test.ts +167 -0
- package/src/__tests__/setup-complexity.test.ts +381 -0
- package/src/__tests__/setup-validate.test.ts +148 -0
- package/src/__tests__/solve-issue-tea-worktree.test.ts +144 -0
- package/src/__tests__/spawn-agent-json.test.ts +100 -0
- package/src/__tests__/step-read.test.ts +632 -0
- package/src/__tests__/step-show-json.test.ts +373 -0
- package/src/__tests__/step-timing.test.ts +392 -0
- package/src/__tests__/store-global-cas.test.ts +308 -0
- package/src/__tests__/store-storage-root.test.ts +49 -0
- package/src/__tests__/store-unified-threads.test.ts +235 -0
- package/src/__tests__/thread-cancel-status.test.ts +138 -0
- package/src/__tests__/thread-list-filters.test.ts +572 -0
- package/src/__tests__/thread-location.test.ts +186 -0
- package/src/__tests__/thread-read-quota.test.ts +613 -0
- package/src/__tests__/thread-read-xml-tags.test.ts +717 -0
- package/src/__tests__/thread-resume.test.ts +710 -0
- package/src/__tests__/thread-show-status.test.ts +317 -0
- package/src/__tests__/thread-start-cwd-cli.test.ts +164 -0
- package/src/__tests__/thread-step-count.test.ts +70 -0
- package/src/__tests__/thread-suspend-step.test.ts +181 -0
- package/src/__tests__/thread-suspended-display.test.ts +287 -0
- package/src/__tests__/thread-test-helpers.ts +37 -0
- package/src/__tests__/thread.test.ts +1025 -0
- package/src/__tests__/validate-semantic.test.ts +474 -0
- package/src/__tests__/workflow-resolution.test.ts +421 -0
- package/src/background/background.ts +147 -0
- package/src/background/index.ts +11 -0
- package/src/background/types.ts +9 -0
- package/src/cli.ts +692 -0
- package/src/commands/config.ts +304 -0
- package/src/commands/log.ts +116 -0
- package/src/commands/prompt.ts +81 -0
- package/src/commands/setup.ts +603 -0
- package/src/commands/shared.ts +227 -0
- package/src/commands/step.ts +343 -0
- package/src/commands/thread-time-parser.ts +23 -0
- package/src/commands/thread.ts +1575 -0
- package/src/commands/workflow.ts +213 -0
- package/src/format.ts +12 -0
- package/src/include.ts +37 -0
- package/src/moderator/__tests__/evaluate.test.ts +199 -0
- package/src/moderator/evaluate.ts +80 -0
- package/src/moderator/index.ts +7 -0
- package/src/moderator/types.ts +24 -0
- package/src/schemas.ts +26 -0
- package/src/store.ts +479 -0
- package/src/validate-semantic.ts +304 -0
- package/src/validate.ts +137 -0
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
_discoverAgents,
|
|
7
|
+
_isBackspace,
|
|
8
|
+
_isTerminator,
|
|
9
|
+
_parseWhichOutput,
|
|
10
|
+
_printModelMenu,
|
|
11
|
+
_printProviderMenu,
|
|
12
|
+
_printValidationResult,
|
|
13
|
+
_resolveModelChoice,
|
|
14
|
+
_resolveProviderChoice,
|
|
15
|
+
_searchPathDirs,
|
|
16
|
+
} from "../commands/setup.js";
|
|
17
|
+
|
|
18
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
19
|
+
// 1a. _searchPathDirs
|
|
20
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe("_searchPathDirs", () => {
|
|
23
|
+
test("returns empty array for empty PATH", async () => {
|
|
24
|
+
const result = await _searchPathDirs("");
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("finds uwf-hermes in a single dir", async () => {
|
|
29
|
+
const dir = mkdirSync(join(tmpdir(), `uwf-test-${Date.now()}`), { recursive: true }) as
|
|
30
|
+
| string
|
|
31
|
+
| undefined;
|
|
32
|
+
const actualDir = dir ?? join(tmpdir(), `uwf-test-${Date.now()}`);
|
|
33
|
+
mkdirSync(actualDir, { recursive: true });
|
|
34
|
+
const filePath = join(actualDir, "uwf-hermes");
|
|
35
|
+
writeFileSync(filePath, "#!/bin/sh\n", { mode: 0o755 });
|
|
36
|
+
const result = await _searchPathDirs(actualDir);
|
|
37
|
+
expect(result).toContain("uwf-hermes");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("skips non-uwf- prefixed binaries", async () => {
|
|
41
|
+
const dir = join(tmpdir(), `uwf-test-${Date.now()}-2`);
|
|
42
|
+
mkdirSync(dir, { recursive: true });
|
|
43
|
+
writeFileSync(join(dir, "hermes"), "#!/bin/sh\n", { mode: 0o755 });
|
|
44
|
+
writeFileSync(join(dir, "uwf-hermes"), "#!/bin/sh\n", { mode: 0o755 });
|
|
45
|
+
const result = await _searchPathDirs(dir);
|
|
46
|
+
expect(result).toEqual(["uwf-hermes"]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("skips entry named exactly 'uwf'", async () => {
|
|
50
|
+
const dir = join(tmpdir(), `uwf-test-${Date.now()}-3`);
|
|
51
|
+
mkdirSync(dir, { recursive: true });
|
|
52
|
+
writeFileSync(join(dir, "uwf"), "#!/bin/sh\n", { mode: 0o755 });
|
|
53
|
+
writeFileSync(join(dir, "uwf-hermes"), "#!/bin/sh\n", { mode: 0o755 });
|
|
54
|
+
const result = await _searchPathDirs(dir);
|
|
55
|
+
expect(result).toEqual(["uwf-hermes"]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("skips non-executable files", async () => {
|
|
59
|
+
const dir = join(tmpdir(), `uwf-test-${Date.now()}-4`);
|
|
60
|
+
mkdirSync(dir, { recursive: true });
|
|
61
|
+
writeFileSync(join(dir, "uwf-foo"), "#!/bin/sh\n", { mode: 0o644 });
|
|
62
|
+
const result = await _searchPathDirs(dir);
|
|
63
|
+
expect(result).toEqual([]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("deduplicates across PATH dirs", async () => {
|
|
67
|
+
const dir1 = join(tmpdir(), `uwf-test-${Date.now()}-5a`);
|
|
68
|
+
const dir2 = join(tmpdir(), `uwf-test-${Date.now()}-5b`);
|
|
69
|
+
mkdirSync(dir1, { recursive: true });
|
|
70
|
+
mkdirSync(dir2, { recursive: true });
|
|
71
|
+
writeFileSync(join(dir1, "uwf-hermes"), "#!/bin/sh\n", { mode: 0o755 });
|
|
72
|
+
writeFileSync(join(dir2, "uwf-hermes"), "#!/bin/sh\n", { mode: 0o755 });
|
|
73
|
+
const result = await _searchPathDirs(`${dir1}:${dir2}`);
|
|
74
|
+
expect(result).toEqual(["uwf-hermes"]);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("returns sorted array", async () => {
|
|
78
|
+
const dir = join(tmpdir(), `uwf-test-${Date.now()}-6`);
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
writeFileSync(join(dir, "uwf-zoo"), "#!/bin/sh\n", { mode: 0o755 });
|
|
81
|
+
writeFileSync(join(dir, "uwf-alpha"), "#!/bin/sh\n", { mode: 0o755 });
|
|
82
|
+
writeFileSync(join(dir, "uwf-mid"), "#!/bin/sh\n", { mode: 0o755 });
|
|
83
|
+
const result = await _searchPathDirs(dir);
|
|
84
|
+
expect(result).toEqual(["uwf-alpha", "uwf-mid", "uwf-zoo"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("skips inaccessible/nonexistent directories silently", async () => {
|
|
88
|
+
const result = await _searchPathDirs("/nonexistent-dir-xyz-abc-12345");
|
|
89
|
+
expect(result).toEqual([]);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
94
|
+
// 1b. _parseWhichOutput
|
|
95
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
describe("_parseWhichOutput", () => {
|
|
98
|
+
test("returns empty array for empty string", () => {
|
|
99
|
+
expect(_parseWhichOutput("")).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("parses single path", () => {
|
|
103
|
+
expect(_parseWhichOutput("/usr/local/bin/uwf-hermes")).toEqual(["uwf-hermes"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("parses multiple paths", () => {
|
|
107
|
+
expect(_parseWhichOutput("/usr/local/bin/uwf-hermes\n/usr/bin/uwf-claude-code")).toEqual([
|
|
108
|
+
"uwf-claude-code",
|
|
109
|
+
"uwf-hermes",
|
|
110
|
+
]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("deduplicates identical basenames from different dirs", () => {
|
|
114
|
+
expect(_parseWhichOutput("/a/uwf-hermes\n/b/uwf-hermes")).toEqual(["uwf-hermes"]);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("skips blank lines", () => {
|
|
118
|
+
expect(_parseWhichOutput("/a/uwf-hermes\n\n/b/uwf-cursor")).toEqual([
|
|
119
|
+
"uwf-cursor",
|
|
120
|
+
"uwf-hermes",
|
|
121
|
+
]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("skips entry named exactly 'uwf'", () => {
|
|
125
|
+
expect(_parseWhichOutput("/usr/bin/uwf")).toEqual([]);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("skips basenames not starting with uwf-", () => {
|
|
129
|
+
expect(_parseWhichOutput("/usr/bin/node")).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("returns sorted array", () => {
|
|
133
|
+
expect(_parseWhichOutput("/a/uwf-zoo\n/a/uwf-alpha")).toEqual(["uwf-alpha", "uwf-zoo"]);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
138
|
+
// 2a. _isTerminator
|
|
139
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe("_isTerminator", () => {
|
|
142
|
+
test("\\n is a terminator", () => {
|
|
143
|
+
expect(_isTerminator("\n")).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
test("\\r is a terminator", () => {
|
|
146
|
+
expect(_isTerminator("\r")).toBe(true);
|
|
147
|
+
});
|
|
148
|
+
test("\\u0004 (EOT) is a terminator", () => {
|
|
149
|
+
expect(_isTerminator("")).toBe(true);
|
|
150
|
+
});
|
|
151
|
+
test("regular char is not a terminator", () => {
|
|
152
|
+
expect(_isTerminator("a")).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
test("empty string is not a terminator", () => {
|
|
155
|
+
expect(_isTerminator("")).toBe(false);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
// 2b. _isBackspace
|
|
161
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
describe("_isBackspace", () => {
|
|
164
|
+
test("\\u007F is a backspace", () => {
|
|
165
|
+
expect(_isBackspace("")).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
test("\\b is a backspace", () => {
|
|
168
|
+
expect(_isBackspace("\b")).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
test("regular char is not a backspace", () => {
|
|
171
|
+
expect(_isBackspace("x")).toBe(false);
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
176
|
+
// 3a. _printProviderMenu
|
|
177
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
describe("_printProviderMenu", () => {
|
|
180
|
+
afterEach(() => {
|
|
181
|
+
vi.restoreAllMocks();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
const providers = [
|
|
185
|
+
{ name: "openai", label: "OpenAI", baseUrl: "https://api.openai.com/v1" },
|
|
186
|
+
{ name: "xai", label: "xAI", baseUrl: "https://api.x.ai/v1" },
|
|
187
|
+
] as const;
|
|
188
|
+
|
|
189
|
+
test("prints correct number of lines (one per provider + custom)", () => {
|
|
190
|
+
const lines: string[] = [];
|
|
191
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
192
|
+
lines.push(msg);
|
|
193
|
+
});
|
|
194
|
+
_printProviderMenu(providers);
|
|
195
|
+
// 2 providers + 1 custom = 3 lines
|
|
196
|
+
expect(lines.length).toBe(3);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("custom option number = providers.length + 1", () => {
|
|
200
|
+
const lines: string[] = [];
|
|
201
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
202
|
+
lines.push(msg);
|
|
203
|
+
});
|
|
204
|
+
_printProviderMenu(providers);
|
|
205
|
+
const lastLine = lines[lines.length - 1] ?? "";
|
|
206
|
+
expect(lastLine).toMatch(/3\)/);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("each provider line contains its label and baseUrl", () => {
|
|
210
|
+
const lines: string[] = [];
|
|
211
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
212
|
+
lines.push(msg);
|
|
213
|
+
});
|
|
214
|
+
_printProviderMenu(providers);
|
|
215
|
+
expect(lines[0]).toContain("OpenAI");
|
|
216
|
+
expect(lines[0]).toContain("https://api.openai.com/v1");
|
|
217
|
+
expect(lines[1]).toContain("xAI");
|
|
218
|
+
expect(lines[1]).toContain("https://api.x.ai/v1");
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
223
|
+
// 3b. _resolveProviderChoice
|
|
224
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
describe("_resolveProviderChoice", () => {
|
|
227
|
+
const providers = [
|
|
228
|
+
{ name: "openai", label: "OpenAI", baseUrl: "https://api.openai.com/v1" },
|
|
229
|
+
{ name: "xai", label: "xAI", baseUrl: "https://api.x.ai/v1" },
|
|
230
|
+
{ name: "deepseek", label: "DeepSeek", baseUrl: "https://api.deepseek.com/v1" },
|
|
231
|
+
] as const;
|
|
232
|
+
|
|
233
|
+
test("valid index 1 returns first provider", () => {
|
|
234
|
+
const result = _resolveProviderChoice("1", providers);
|
|
235
|
+
expect(result).toEqual({ providerName: "openai", baseUrl: "https://api.openai.com/v1" });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("valid index N (last preset) returns last provider", () => {
|
|
239
|
+
const result = _resolveProviderChoice("3", providers);
|
|
240
|
+
expect(result).toEqual({ providerName: "deepseek", baseUrl: "https://api.deepseek.com/v1" });
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("index providers.length+1 (custom) returns null", () => {
|
|
244
|
+
const result = _resolveProviderChoice("4", providers);
|
|
245
|
+
expect(result).toBeNull();
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("non-numeric string returns null", () => {
|
|
249
|
+
expect(_resolveProviderChoice("abc", providers)).toBeNull();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("0 returns null (out of range)", () => {
|
|
253
|
+
expect(_resolveProviderChoice("0", providers)).toBeNull();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("N+2 returns null (out of range)", () => {
|
|
257
|
+
expect(_resolveProviderChoice("5", providers)).toBeNull();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("negative number returns null", () => {
|
|
261
|
+
expect(_resolveProviderChoice("-1", providers)).toBeNull();
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
266
|
+
// 3c. _resolveModelChoice
|
|
267
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
describe("_resolveModelChoice", () => {
|
|
270
|
+
test("numeric input within range returns model at that index", () => {
|
|
271
|
+
expect(_resolveModelChoice("2", ["a", "b", "c"])).toBe("b");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("numeric input out of range returns input as-is", () => {
|
|
275
|
+
expect(_resolveModelChoice("5", ["a"])).toBe("5");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test("non-numeric input returns input as-is", () => {
|
|
279
|
+
expect(_resolveModelChoice("gpt-4o", ["a", "b"])).toBe("gpt-4o");
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("numeric input 1 returns first model", () => {
|
|
283
|
+
expect(_resolveModelChoice("1", ["alpha", "beta"])).toBe("alpha");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("empty models list with numeric input returns input as-is", () => {
|
|
287
|
+
expect(_resolveModelChoice("1", [])).toBe("1");
|
|
288
|
+
});
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
292
|
+
// 3d. _printModelMenu
|
|
293
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe("_printModelMenu", () => {
|
|
296
|
+
afterEach(() => {
|
|
297
|
+
vi.restoreAllMocks();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("prints all models — each model name appears in output", () => {
|
|
301
|
+
const output: string[] = [];
|
|
302
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
303
|
+
output.push(msg);
|
|
304
|
+
});
|
|
305
|
+
const models = ["model-a", "model-b", "model-c"];
|
|
306
|
+
_printModelMenu(models, 100);
|
|
307
|
+
const combined = output.join("\n");
|
|
308
|
+
for (const m of models) {
|
|
309
|
+
expect(combined).toContain(m);
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("single column when termCols is very small", () => {
|
|
314
|
+
const output: string[] = [];
|
|
315
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
316
|
+
output.push(msg);
|
|
317
|
+
});
|
|
318
|
+
_printModelMenu(["a", "b", "c"], 1);
|
|
319
|
+
// Each model on its own row → 3 lines
|
|
320
|
+
expect(output.length).toBe(3);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("wide terminal fits multiple columns", () => {
|
|
324
|
+
const output: string[] = [];
|
|
325
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
326
|
+
output.push(msg);
|
|
327
|
+
});
|
|
328
|
+
const models = Array.from({ length: 6 }, (_, i) => `m${i}`);
|
|
329
|
+
_printModelMenu(models, 200);
|
|
330
|
+
// With wide terminal and short names, should fit in fewer than 6 rows
|
|
331
|
+
expect(output.length).toBeLessThan(6);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
// 3e. _printValidationResult
|
|
337
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
describe("_printValidationResult", () => {
|
|
340
|
+
afterEach(() => {
|
|
341
|
+
vi.restoreAllMocks();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("ok=true prints success message containing '✓'", () => {
|
|
345
|
+
const lines: string[] = [];
|
|
346
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
347
|
+
lines.push(msg);
|
|
348
|
+
});
|
|
349
|
+
_printValidationResult({ ok: true, error: null });
|
|
350
|
+
expect(lines.join("\n")).toContain("✓");
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("ok=false prints warning message containing '⚠'", () => {
|
|
354
|
+
const lines: string[] = [];
|
|
355
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
356
|
+
lines.push(msg);
|
|
357
|
+
});
|
|
358
|
+
_printValidationResult({ ok: false, error: "HTTP 401" });
|
|
359
|
+
expect(lines.join("\n")).toContain("⚠");
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("ok=false includes the error string in output", () => {
|
|
363
|
+
const lines: string[] = [];
|
|
364
|
+
vi.spyOn(console, "log").mockImplementation((msg: string) => {
|
|
365
|
+
lines.push(msg);
|
|
366
|
+
});
|
|
367
|
+
_printValidationResult({ ok: false, error: "HTTP 401" });
|
|
368
|
+
expect(lines.join("\n")).toContain("HTTP 401");
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
373
|
+
// 4. Regression
|
|
374
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
describe("_discoverAgents regression", () => {
|
|
377
|
+
test("returns an array (may be empty) — never throws", async () => {
|
|
378
|
+
const result = await _discoverAgents();
|
|
379
|
+
expect(Array.isArray(result)).toBe(true);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
|
5
|
+
import { cmdSetup, validateModel } from "../commands/setup.js";
|
|
6
|
+
|
|
7
|
+
describe("validateModel", () => {
|
|
8
|
+
const BASE_URL = "https://api.example.com/v1";
|
|
9
|
+
const API_KEY = "sk-test-key";
|
|
10
|
+
const MODEL = "test-model";
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
vi.restoreAllMocks();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("success path — returns ok on 200", async () => {
|
|
17
|
+
const mockFetch = vi
|
|
18
|
+
.spyOn(globalThis, "fetch")
|
|
19
|
+
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
20
|
+
|
|
21
|
+
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
22
|
+
|
|
23
|
+
expect(result).toEqual({ ok: true, value: undefined });
|
|
24
|
+
expect(mockFetch).toHaveBeenCalledTimes(1);
|
|
25
|
+
|
|
26
|
+
const [url, opts] = mockFetch.mock.calls[0]!;
|
|
27
|
+
expect(url).toBe(`${BASE_URL}/chat/completions`);
|
|
28
|
+
expect((opts as RequestInit).headers).toEqual(
|
|
29
|
+
expect.objectContaining({ Authorization: `Bearer ${API_KEY}` }),
|
|
30
|
+
);
|
|
31
|
+
const body = JSON.parse((opts as RequestInit).body as string);
|
|
32
|
+
expect(body).toEqual({
|
|
33
|
+
model: MODEL,
|
|
34
|
+
messages: [{ role: "user", content: "hi" }],
|
|
35
|
+
max_tokens: 1,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("HTTP 401 — returns error containing 401", async () => {
|
|
40
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
41
|
+
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
45
|
+
|
|
46
|
+
expect(result.ok).toBe(false);
|
|
47
|
+
if (!result.ok) {
|
|
48
|
+
expect(result.error).toContain("401");
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("HTTP 404 — returns error containing 404", async () => {
|
|
53
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
54
|
+
new Response("Not Found", { status: 404, statusText: "Not Found" }),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
58
|
+
|
|
59
|
+
expect(result.ok).toBe(false);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
expect(result.error).toContain("404");
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("network timeout — returns error mentioning timeout", async () => {
|
|
66
|
+
const err = new DOMException("signal timed out", "AbortError");
|
|
67
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(err);
|
|
68
|
+
|
|
69
|
+
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
70
|
+
|
|
71
|
+
expect(result.ok).toBe(false);
|
|
72
|
+
if (!result.ok) {
|
|
73
|
+
expect(result.error.toLowerCase()).toMatch(/timeout|timed out/);
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("network error (DNS/connection) — returns error mentioning connectivity", async () => {
|
|
78
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new TypeError("fetch failed"));
|
|
79
|
+
|
|
80
|
+
const result = await validateModel(BASE_URL, API_KEY, MODEL);
|
|
81
|
+
|
|
82
|
+
expect(result.ok).toBe(false);
|
|
83
|
+
if (!result.ok) {
|
|
84
|
+
expect(result.error.toLowerCase()).toMatch(/connect|reach|network/);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("request body correctness", async () => {
|
|
89
|
+
const mockFetch = vi
|
|
90
|
+
.spyOn(globalThis, "fetch")
|
|
91
|
+
.mockResolvedValue(new Response(JSON.stringify({}), { status: 200 }));
|
|
92
|
+
|
|
93
|
+
await validateModel(BASE_URL, API_KEY, "my-special-model");
|
|
94
|
+
|
|
95
|
+
const body = JSON.parse((mockFetch.mock.calls[0]![1] as RequestInit).body as string);
|
|
96
|
+
expect(body).toEqual({
|
|
97
|
+
model: "my-special-model",
|
|
98
|
+
messages: [{ role: "user", content: "hi" }],
|
|
99
|
+
max_tokens: 1,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("cmdSetup with validation", () => {
|
|
105
|
+
let storageRoot: string;
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
storageRoot = await mkdtemp(join(tmpdir(), "uwf-setup-validate-"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
afterEach(async () => {
|
|
112
|
+
vi.restoreAllMocks();
|
|
113
|
+
await rm(storageRoot, { recursive: true, force: true });
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const setupArgs = () => ({
|
|
117
|
+
provider: "testprovider",
|
|
118
|
+
baseUrl: "https://api.test.com/v1",
|
|
119
|
+
apiKey: "sk-test",
|
|
120
|
+
model: "test-model",
|
|
121
|
+
storageRoot,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("includes validation result on success", async () => {
|
|
125
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
126
|
+
new Response(JSON.stringify({}), { status: 200 }),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const result = await cmdSetup(setupArgs());
|
|
130
|
+
|
|
131
|
+
expect(result.validation).toEqual({ ok: true, value: undefined });
|
|
132
|
+
// Config file should still be written
|
|
133
|
+
expect(result.configPath).toBeTruthy();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("includes validation failure — config still saved", async () => {
|
|
137
|
+
vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
138
|
+
new Response("Unauthorized", { status: 401, statusText: "Unauthorized" }),
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const result = await cmdSetup(setupArgs());
|
|
142
|
+
|
|
143
|
+
expect(result.validation).toBeDefined();
|
|
144
|
+
expect((result.validation as { ok: boolean }).ok).toBe(false);
|
|
145
|
+
// Config file should still be written despite validation failure
|
|
146
|
+
expect(result.configPath).toBeTruthy();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import type { WorkflowPayload } from "@united-workforce/protocol";
|
|
5
|
+
import { describe, expect, test } from "vitest";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Test: Issue #474 - tea pr create fails in git worktree directories
|
|
10
|
+
*
|
|
11
|
+
* This test verifies that the solve-issue workflow's committer role
|
|
12
|
+
* uses direct Gitea API calls via curl instead of tea pr create,
|
|
13
|
+
* which fixes the "path segment [0] is empty" error in worktree directories.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
describe("solve-issue workflow: Gitea API PR creation", () => {
|
|
17
|
+
// Navigate up from packages/cli/src/__tests__ to repo root
|
|
18
|
+
const workflowPath = join(
|
|
19
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
20
|
+
"..",
|
|
21
|
+
"..",
|
|
22
|
+
"..",
|
|
23
|
+
"..",
|
|
24
|
+
".workflows",
|
|
25
|
+
"solve-issue.yaml",
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
test("committer procedure should use curl API instead of tea pr create", async () => {
|
|
29
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
30
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
31
|
+
|
|
32
|
+
expect(workflow.roles.committer).toBeDefined();
|
|
33
|
+
const committerProcedure = workflow.roles.committer?.procedure;
|
|
34
|
+
expect(committerProcedure).toBeDefined();
|
|
35
|
+
|
|
36
|
+
// Verify the procedure uses curl API, not tea pr create
|
|
37
|
+
expect(committerProcedure).toContain("curl");
|
|
38
|
+
expect(committerProcedure).toContain("api/v1/repos");
|
|
39
|
+
expect(committerProcedure).toContain("/pulls");
|
|
40
|
+
|
|
41
|
+
// Verify it explicitly warns against tea pr create
|
|
42
|
+
expect(committerProcedure).toMatch(/do NOT use.*tea pr create/i);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("committer procedure should reference repoRemote from task prompt", async () => {
|
|
46
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
47
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
48
|
+
|
|
49
|
+
const committerProcedure = workflow.roles.committer?.procedure;
|
|
50
|
+
expect(committerProcedure).toBeDefined();
|
|
51
|
+
|
|
52
|
+
// Verify the procedure mentions repoRemote is provided in task prompt
|
|
53
|
+
expect(committerProcedure).toMatch(/repo remote.*provided.*task prompt/i);
|
|
54
|
+
expect(committerProcedure).toMatch(/owner\/repo/i);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("committer procedure should include error handling for curl failures", async () => {
|
|
58
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
59
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
60
|
+
|
|
61
|
+
const committerProcedure = workflow.roles.committer?.procedure;
|
|
62
|
+
expect(committerProcedure).toBeDefined();
|
|
63
|
+
|
|
64
|
+
// Verify the procedure includes error handling guidance for curl
|
|
65
|
+
// This ensures we capture failures and provide actionable output
|
|
66
|
+
expect(committerProcedure).toMatch(/error|fail/i);
|
|
67
|
+
expect(committerProcedure).toContain("hook_failed");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("workflow should be parseable as valid WorkflowPayload", async () => {
|
|
71
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
72
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
73
|
+
|
|
74
|
+
// Basic structure validation
|
|
75
|
+
expect(workflow.name).toBe("solve-issue");
|
|
76
|
+
expect(workflow.roles).toBeDefined();
|
|
77
|
+
expect(workflow.graph).toBeDefined();
|
|
78
|
+
|
|
79
|
+
// Verify committer role exists with required fields
|
|
80
|
+
expect(workflow.roles.committer).toBeDefined();
|
|
81
|
+
expect(workflow.roles.committer?.description).toBeDefined();
|
|
82
|
+
expect(workflow.roles.committer?.goal).toBeDefined();
|
|
83
|
+
expect(workflow.roles.committer?.procedure).toBeDefined();
|
|
84
|
+
expect(workflow.roles.committer?.output).toBeDefined();
|
|
85
|
+
expect(workflow.roles.committer?.frontmatter).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("committer frontmatter schema should be oneOf with $status discriminant", async () => {
|
|
89
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
90
|
+
// Parse as any to access the raw YAML structure (frontmatter is inline JSON Schema in YAML)
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
92
|
+
const workflow = parse(yamlContent) as any;
|
|
93
|
+
const frontmatter = workflow.roles.committer?.frontmatter;
|
|
94
|
+
expect(frontmatter).toBeDefined();
|
|
95
|
+
expect(frontmatter?.oneOf).toBeDefined();
|
|
96
|
+
const committedVariant = frontmatter.oneOf.find(
|
|
97
|
+
(v: any) => v.properties?.$status?.const === "committed",
|
|
98
|
+
);
|
|
99
|
+
expect(committedVariant).toBeDefined();
|
|
100
|
+
expect(committedVariant.required).toContain("$status");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("developer procedure should include mandatory verification step", async () => {
|
|
104
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
105
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
106
|
+
|
|
107
|
+
const developerProcedure = workflow.roles.developer?.procedure;
|
|
108
|
+
expect(developerProcedure).toBeDefined();
|
|
109
|
+
|
|
110
|
+
// Verify the procedure includes mandatory verification step
|
|
111
|
+
expect(developerProcedure).toContain("MANDATORY VERIFICATION");
|
|
112
|
+
expect(developerProcedure).toContain("git branch --show-current");
|
|
113
|
+
expect(developerProcedure).toContain("git status");
|
|
114
|
+
expect(developerProcedure).toMatch(/ls -la|verify.*exist/i);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("reviewer procedure should enforce worktree path verification", async () => {
|
|
118
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
119
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
120
|
+
|
|
121
|
+
const reviewerProcedure = workflow.roles.reviewer?.procedure;
|
|
122
|
+
expect(reviewerProcedure).toBeDefined();
|
|
123
|
+
|
|
124
|
+
// Verify the procedure includes critical enforcement
|
|
125
|
+
expect(reviewerProcedure).toContain("CRITICAL");
|
|
126
|
+
expect(reviewerProcedure).toMatch(/cd.*pwd/);
|
|
127
|
+
expect(reviewerProcedure).toContain(
|
|
128
|
+
"Do NOT report results without running the actual commands",
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("developer procedure should include test debugging escalation", async () => {
|
|
133
|
+
const yamlContent = await readFile(workflowPath, "utf-8");
|
|
134
|
+
const workflow = parse(yamlContent) as WorkflowPayload;
|
|
135
|
+
|
|
136
|
+
const developerProcedure = workflow.roles.developer?.procedure;
|
|
137
|
+
expect(developerProcedure).toBeDefined();
|
|
138
|
+
|
|
139
|
+
// Verify the procedure includes test failure guidance
|
|
140
|
+
expect(developerProcedure).toMatch(/tests fail.*first run/i);
|
|
141
|
+
expect(developerProcedure).toMatch(/3 test cycles|after 3 attempts/i);
|
|
142
|
+
expect(developerProcedure).toContain("$status=failed");
|
|
143
|
+
});
|
|
144
|
+
});
|