@aion0/forge 0.10.6 → 0.10.17

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.
Files changed (41) hide show
  1. package/RELEASE_NOTES.md +5 -5
  2. package/app/api/public-info/[resource]/route.ts +40 -0
  3. package/app/api/skills/install-local/route.ts +2 -1
  4. package/cli/mw.mjs +11 -21
  5. package/components/SettingsModal.tsx +42 -33
  6. package/components/WorkspaceView.tsx +5 -3
  7. package/lib/agents/index.ts +8 -9
  8. package/lib/agents/known-models.ts +75 -0
  9. package/lib/agents/migrate.ts +14 -3
  10. package/lib/dirs.ts +6 -26
  11. package/lib/public-info/fetch.ts +116 -0
  12. package/lib/public-info/types.ts +38 -0
  13. package/lib/public-info/use-models-registry.ts +66 -0
  14. package/lib/settings.ts +34 -4
  15. package/lib/skills.ts +2 -2
  16. package/lib/workspace/watch-manager.ts +5 -1
  17. package/package.json +1 -1
  18. package/lib/__tests__/foreach-batch-yaml.test.ts +0 -33
  19. package/lib/__tests__/foreach-before.test.ts +0 -201
  20. package/lib/__tests__/foreach-parse.test.ts +0 -114
  21. package/lib/__tests__/foreach-snapshot.test.ts +0 -112
  22. package/lib/__tests__/foreach-source.test.ts +0 -105
  23. package/lib/__tests__/foreach-template.test.ts +0 -112
  24. package/lib/workspace/__tests__/state-machine.test.ts +0 -388
  25. package/lib/workspace/__tests__/workspace.test.ts +0 -311
  26. package/scripts/bench/README.md +0 -66
  27. package/scripts/bench/results/.gitignore +0 -2
  28. package/scripts/bench/run.ts +0 -635
  29. package/scripts/bench/tasks/01-text-utils/task.md +0 -26
  30. package/scripts/bench/tasks/01-text-utils/validator.sh +0 -46
  31. package/scripts/bench/tasks/02-pagination/setup.sh +0 -19
  32. package/scripts/bench/tasks/02-pagination/task.md +0 -48
  33. package/scripts/bench/tasks/02-pagination/validator.sh +0 -69
  34. package/scripts/bench/tasks/03-bug-fix/setup.sh +0 -82
  35. package/scripts/bench/tasks/03-bug-fix/task.md +0 -30
  36. package/scripts/bench/tasks/03-bug-fix/validator.sh +0 -29
  37. package/scripts/test-agents-migrate.ts +0 -149
  38. package/scripts/test-mantis.ts +0 -223
  39. package/scripts/test-memory-local.ts +0 -139
  40. package/scripts/test-memory-upsert.ts +0 -106
  41. package/scripts/verify-usage.ts +0 -178
@@ -1,46 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Validator for text utility task.
3
- # Runs in harness_test project root. Exits 0 = pass, non-zero = fail.
4
- set -e
5
-
6
- PROJECT_ROOT="${1:-~/Projects/sandbox}"
7
- cd "$PROJECT_ROOT/src" || { echo "FAIL: src/ directory not found"; exit 1; }
8
-
9
- # 1. Check files exist
10
- [ -f utils/text.js ] || { echo "FAIL: utils/text.js missing"; exit 1; }
11
- [ -f utils/text.test.js ] || { echo "FAIL: utils/text.test.js missing"; exit 1; }
12
-
13
- # 2. Check exports
14
- grep -q "export.*capitalize" utils/text.js || { echo "FAIL: capitalize not exported"; exit 1; }
15
- grep -q "export.*reverseWords" utils/text.js || { echo "FAIL: reverseWords not exported"; exit 1; }
16
-
17
- # 3. Run tests
18
- node --test utils/text.test.js 2>&1 | tee /tmp/text-test-output.txt
19
- TEST_EXIT=${PIPESTATUS[0]}
20
- if [ "$TEST_EXIT" != "0" ]; then
21
- echo "FAIL: tests failed (exit=$TEST_EXIT)"
22
- exit 1
23
- fi
24
-
25
- # 4. Additional smoke test — behavior verification independent of agent's tests
26
- node -e "
27
- import('./utils/text.js').then(m => {
28
- const assert = require('node:assert/strict');
29
- // capitalize
30
- assert.equal(m.capitalize('hello'), 'Hello', 'capitalize basic');
31
- assert.equal(m.capitalize('a'), 'A', 'capitalize single char');
32
- try { m.capitalize(''); assert.fail('expected throw on empty'); } catch (e) { assert.ok(e instanceof TypeError); }
33
- try { m.capitalize(null); assert.fail('expected throw on null'); } catch (e) { assert.ok(e instanceof TypeError); }
34
- try { m.capitalize(123); assert.fail('expected throw on number'); } catch (e) { assert.ok(e instanceof TypeError); }
35
- // reverseWords
36
- assert.equal(m.reverseWords('hello world'), 'world hello');
37
- assert.equal(m.reverseWords(' a b c '), 'c b a');
38
- assert.equal(m.reverseWords(''), '');
39
- assert.equal(m.reverseWords('single'), 'single');
40
- try { m.reverseWords(null); assert.fail('expected throw'); } catch (e) { assert.ok(e instanceof TypeError); }
41
- console.log('SMOKE_TEST_PASSED');
42
- }).catch(err => { console.error('SMOKE_TEST_FAILED:', err.message); process.exit(1); });
43
- " || { echo "FAIL: smoke test failed"; exit 1; }
44
-
45
- echo "PASS"
46
- exit 0
@@ -1,19 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Create a basic user list module without pagination.
3
- set -e
4
- PROJECT="${1:-~/Projects/sandbox}"
5
- mkdir -p "$PROJECT/src/api"
6
-
7
- cat > "$PROJECT/src/api/users.js" <<'EOF'
8
- const USERS = Array.from({ length: 127 }, (_, i) => ({
9
- id: i + 1,
10
- name: `User ${i + 1}`,
11
- email: `user${i + 1}@example.com`,
12
- }));
13
-
14
- export function listUsers() {
15
- return USERS;
16
- }
17
- EOF
18
-
19
- echo "Setup complete: created src/api/users.js with 127 users and a listUsers() function."
@@ -1,48 +0,0 @@
1
- # Task: Add Pagination to User List
2
-
3
- The file `src/api/users.js` currently has a `listUsers()` function that returns all users. Add pagination support.
4
-
5
- ## Requirements
6
-
7
- Replace `listUsers()` (or add a new function) with a paginated version:
8
-
9
- ```
10
- listUsers({ page = 1, pageSize = 20 } = {})
11
- ```
12
-
13
- **Return format**:
14
- ```js
15
- {
16
- items: [...], // users on the current page
17
- total: 127, // total number of users
18
- page: 1, // current page (1-indexed)
19
- pageSize: 20, // page size (after validation)
20
- totalPages: 7, // Math.ceil(total / pageSize)
21
- hasNext: true, // true if more pages exist
22
- hasPrev: false // true if page > 1
23
- }
24
- ```
25
-
26
- ## Validation Rules
27
-
28
- - `page` must be integer ≥ 1. If invalid (not a number, < 1, NaN, float), throw `RangeError`.
29
- - `pageSize` must be integer in [1, 100]. If invalid, throw `RangeError`.
30
- - If `page` exceeds available pages, return empty `items` array but still return correct `total`, `page`, `pageSize`, `totalPages`, `hasNext: false`, `hasPrev: true`.
31
-
32
- ## Test File
33
-
34
- Also create `src/api/users.test.js` using `node:test` and `node:assert/strict` covering:
35
- - Default params return page 1 with 20 items
36
- - Page 2 returns items 21-40
37
- - Last page (page 7) returns items 121-127
38
- - Page 8 returns empty items but correct metadata
39
- - Custom pageSize (e.g., 50)
40
- - Invalid page (0, -1, 'abc', 1.5, NaN) throws RangeError
41
- - Invalid pageSize (0, 101, 'abc', 1.5) throws RangeError
42
-
43
- ## Constraints
44
-
45
- - Keep ES module syntax
46
- - No external deps
47
- - Preserve the existing USERS array
48
- - Tests must pass via: `cd src && node --test api/users.test.js`
@@ -1,69 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- PROJECT="${1:-~/Projects/sandbox}"
4
- cd "$PROJECT/src"
5
-
6
- [ -f api/users.js ] || { echo "FAIL: api/users.js missing"; exit 1; }
7
- [ -f api/users.test.js ] || { echo "FAIL: api/users.test.js missing"; exit 1; }
8
- grep -q "export function listUsers\|export const listUsers\|export { listUsers" api/users.js || { echo "FAIL: listUsers not exported"; exit 1; }
9
-
10
- # Run agent's tests
11
- node --test api/users.test.js 2>&1 | tee /tmp/paginate-test-output.txt
12
- TEST_EXIT=${PIPESTATUS[0]}
13
- [ "$TEST_EXIT" = "0" ] || { echo "FAIL: agent tests failed"; exit 1; }
14
-
15
- # Independent smoke test
16
- node -e "
17
- import('./api/users.js').then(m => {
18
- const assert = require('node:assert/strict');
19
- // Default: page 1, 20 items
20
- let r = m.listUsers();
21
- assert.equal(r.items.length, 20, 'default pageSize 20');
22
- assert.equal(r.items[0].id, 1);
23
- assert.equal(r.total, 127);
24
- assert.equal(r.page, 1);
25
- assert.equal(r.pageSize, 20);
26
- assert.equal(r.totalPages, 7);
27
- assert.equal(r.hasNext, true);
28
- assert.equal(r.hasPrev, false);
29
-
30
- // Page 2
31
- r = m.listUsers({ page: 2 });
32
- assert.equal(r.items[0].id, 21);
33
- assert.equal(r.items.length, 20);
34
- assert.equal(r.hasPrev, true);
35
-
36
- // Last page (127 / 20 = 6.35 → 7 pages; page 7 has 7 items)
37
- r = m.listUsers({ page: 7 });
38
- assert.equal(r.items.length, 7, 'last page has 7 items');
39
- assert.equal(r.items[0].id, 121);
40
- assert.equal(r.hasNext, false);
41
-
42
- // Page 8 (beyond) — empty but correct metadata
43
- r = m.listUsers({ page: 8 });
44
- assert.equal(r.items.length, 0, 'page beyond: empty items');
45
- assert.equal(r.total, 127);
46
- assert.equal(r.totalPages, 7);
47
- assert.equal(r.hasNext, false);
48
-
49
- // Custom pageSize
50
- r = m.listUsers({ page: 1, pageSize: 50 });
51
- assert.equal(r.items.length, 50);
52
- assert.equal(r.totalPages, 3);
53
-
54
- // Invalid page
55
- for (const p of [0, -1, 'abc', 1.5, NaN]) {
56
- try { m.listUsers({ page: p }); assert.fail('expected RangeError for page=' + p); }
57
- catch (e) { assert.ok(e instanceof RangeError, 'page=' + p + ' should throw RangeError, got: ' + e.constructor.name); }
58
- }
59
- // Invalid pageSize
60
- for (const ps of [0, 101, 'abc', 1.5]) {
61
- try { m.listUsers({ page: 1, pageSize: ps }); assert.fail('expected RangeError for pageSize=' + ps); }
62
- catch (e) { assert.ok(e instanceof RangeError, 'pageSize=' + ps + ' should throw RangeError'); }
63
- }
64
- console.log('SMOKE_TEST_PASSED');
65
- }).catch(err => { console.error('SMOKE_TEST_FAILED:', err.message); process.exit(1); });
66
- " || { echo "FAIL: smoke test failed"; exit 1; }
67
-
68
- echo "PASS"
69
- exit 0
@@ -1,82 +0,0 @@
1
- #!/usr/bin/env bash
2
- # Create a date range calculator with 2 bugs.
3
- set -e
4
- PROJECT="${1:-~/Projects/sandbox}"
5
- mkdir -p "$PROJECT/src/lib" "$PROJECT/src/lib/__tests__"
6
-
7
- cat > "$PROJECT/src/lib/dateRange.js" <<'EOF'
8
- // Compute the inclusive number of days between two YYYY-MM-DD dates.
9
- // Returns a positive integer. If end is before start, throws RangeError.
10
- export function daysBetween(startStr, endStr) {
11
- if (typeof startStr !== 'string' || typeof endStr !== 'string') {
12
- throw new TypeError('daysBetween expects two YYYY-MM-DD strings');
13
- }
14
- const start = new Date(startStr);
15
- const end = new Date(endStr);
16
- if (Number.isNaN(start.getTime()) || Number.isNaN(end.getTime())) {
17
- throw new TypeError('invalid date format');
18
- }
19
- if (end < start) throw new RangeError('end before start');
20
- // BUG: missing +1 to be inclusive of both endpoints
21
- return Math.floor((end - start) / (1000 * 60 * 60 * 24));
22
- }
23
-
24
- // Return array of YYYY-MM-DD strings from start to end (inclusive).
25
- export function dateRange(startStr, endStr) {
26
- const days = daysBetween(startStr, endStr);
27
- const result = [];
28
- const current = new Date(startStr);
29
- // BUG: loop condition uses < instead of <=, excluding final day
30
- for (let i = 0; i < days; i++) {
31
- result.push(current.toISOString().slice(0, 10));
32
- current.setDate(current.getDate() + 1);
33
- }
34
- return result;
35
- }
36
- EOF
37
-
38
- cat > "$PROJECT/src/lib/__tests__/dateRange.test.js" <<'EOF'
39
- import { test } from 'node:test';
40
- import assert from 'node:assert/strict';
41
- import { daysBetween, dateRange } from '../dateRange.js';
42
-
43
- test('daysBetween: same day returns 1', () => {
44
- assert.equal(daysBetween('2026-01-01', '2026-01-01'), 1);
45
- });
46
-
47
- test('daysBetween: one day apart returns 2', () => {
48
- assert.equal(daysBetween('2026-01-01', '2026-01-02'), 2);
49
- });
50
-
51
- test('daysBetween: one week', () => {
52
- assert.equal(daysBetween('2026-01-01', '2026-01-07'), 7);
53
- });
54
-
55
- test('daysBetween: end before start throws RangeError', () => {
56
- assert.throws(() => daysBetween('2026-01-05', '2026-01-01'), RangeError);
57
- });
58
-
59
- test('daysBetween: non-string throws TypeError', () => {
60
- assert.throws(() => daysBetween(20260101, '2026-01-02'), TypeError);
61
- });
62
-
63
- test('dateRange: single day returns array with one date', () => {
64
- assert.deepEqual(dateRange('2026-01-01', '2026-01-01'), ['2026-01-01']);
65
- });
66
-
67
- test('dateRange: three days', () => {
68
- assert.deepEqual(
69
- dateRange('2026-01-01', '2026-01-03'),
70
- ['2026-01-01', '2026-01-02', '2026-01-03']
71
- );
72
- });
73
-
74
- test('dateRange: includes both endpoints', () => {
75
- const r = dateRange('2026-03-30', '2026-04-02');
76
- assert.equal(r.length, 4);
77
- assert.equal(r[0], '2026-03-30');
78
- assert.equal(r[r.length - 1], '2026-04-02');
79
- });
80
- EOF
81
-
82
- echo "Setup complete: created src/lib/dateRange.js (with 2 bugs) and tests that currently fail."
@@ -1,30 +0,0 @@
1
- # Task: Fix Bugs in dateRange Module
2
-
3
- The file `src/lib/dateRange.js` has 2 bugs. The existing test file `src/lib/__tests__/dateRange.test.js` describes the expected behavior.
4
-
5
- ## Your job
6
-
7
- 1. Run the existing tests — several will fail. Identify what's wrong.
8
- 2. Fix both bugs in `src/lib/dateRange.js`.
9
- 3. Do NOT modify the test file. The tests correctly express the expected behavior.
10
- 4. Do NOT change the function signatures or add new functions.
11
- 5. After fixing, all tests must pass.
12
-
13
- ## Verify
14
-
15
- ```bash
16
- cd src && node --test lib/__tests__/dateRange.test.js
17
- ```
18
-
19
- All tests should pass.
20
-
21
- ## Hints
22
-
23
- - `daysBetween('2026-01-01', '2026-01-01')` should return `1` (inclusive count)
24
- - `dateRange('2026-01-01', '2026-01-03')` should return all 3 days including both endpoints
25
-
26
- ## Constraints
27
-
28
- - Minimal diff — fix only what's broken
29
- - Keep the functions pure
30
- - No new dependencies
@@ -1,29 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -e
3
- PROJECT="${1:-~/Projects/sandbox}"
4
- cd "$PROJECT/src"
5
-
6
- [ -f lib/dateRange.js ] || { echo "FAIL: lib/dateRange.js missing (agent deleted it?)"; exit 1; }
7
- [ -f lib/__tests__/dateRange.test.js ] || { echo "FAIL: test file missing (agent deleted it?)"; exit 1; }
8
-
9
- # Run the existing (unmodified) tests
10
- node --test lib/__tests__/dateRange.test.js 2>&1 | tee /tmp/bugfix-test-output.txt
11
- TEST_EXIT=${PIPESTATUS[0]}
12
- [ "$TEST_EXIT" = "0" ] || { echo "FAIL: tests still failing after fix"; exit 1; }
13
-
14
- # Extra smoke: verify functions exist and behave
15
- node -e "
16
- import('./lib/dateRange.js').then(m => {
17
- const assert = require('node:assert/strict');
18
- assert.equal(m.daysBetween('2026-01-01', '2026-01-01'), 1);
19
- assert.equal(m.daysBetween('2026-01-01', '2026-01-10'), 10);
20
- const r = m.dateRange('2026-01-01', '2026-01-05');
21
- assert.equal(r.length, 5, 'should include both endpoints');
22
- assert.equal(r[0], '2026-01-01');
23
- assert.equal(r[4], '2026-01-05');
24
- console.log('SMOKE_TEST_PASSED');
25
- }).catch(err => { console.error('SMOKE_TEST_FAILED:', err.message); process.exit(1); });
26
- " || { echo "FAIL: smoke test failed"; exit 1; }
27
-
28
- echo "PASS"
29
- exit 0
@@ -1,149 +0,0 @@
1
- /**
2
- * Migration smoke test — feed several known-shape settings through
3
- * migrateAgentsFlatten and assert post-state. Runs offline, no real
4
- * data needed.
5
- *
6
- * npx tsx scripts/test-agents-migrate.ts
7
- */
8
-
9
- import { migrateAgentsFlatten } from '../lib/agents/migrate';
10
- import type { Settings } from '../lib/settings';
11
-
12
- let failures = 0;
13
- const fail = (msg: string) => { console.log(` ✗ ${msg}`); failures += 1; };
14
- const pass = (msg: string) => console.log(` ✓ ${msg}`);
15
-
16
- function baseSettings(): Settings {
17
- return {
18
- projectRoots: [], docRoots: [], claudePath: '', claudeHome: '',
19
- telegramBotToken: '', telegramChatId: '',
20
- notifyOnComplete: true, notifyOnFailure: true,
21
- tunnelAutoStart: false, telegramTunnelPassword: '',
22
- taskModel: 'default', pipelineModel: 'default', telegramModel: 'sonnet',
23
- skipPermissions: false, manageClaudeConfig: true,
24
- notificationRetentionDays: 30,
25
- skillsRepoUrl: '', connectorsRepoUrl: '', workflowRepoUrl: '',
26
- maxConcurrentPipelines: 5,
27
- displayName: '', displayEmail: '', favoriteProjects: [],
28
- defaultAgent: 'claude', telegramAgent: '', docsAgent: '', chatAgent: '',
29
- temperUrl: '', temperKey: '', temperNamespace: '', memoryBackend: 'auto',
30
- agents: {}, apiProfiles: {}, mcpServers: {}, timezone: '',
31
- smtpHost: '', smtpPort: 587, smtpSecure: false, smtpUser: '', smtpPassword: '', smtpFrom: '',
32
- pipelineTmpCleanDoneImmediate: true,
33
- pipelineTmpKeepFailedDays: 3, pipelineTmpKeepCancelledDays: 3,
34
- pipelineTmpGcIntervalHours: 6,
35
- } as Settings;
36
- }
37
-
38
- // ── Test 1: type='api' moves to apiProfiles ──────────────────────────
39
- {
40
- console.log('Test 1 — API profile moves out of agents');
41
- const s = baseSettings();
42
- s.agents = {
43
- claude: { enabled: true, path: '/usr/local/bin/claude' },
44
- 'forti-api': { type: 'api', provider: 'litellm', model: 'DeepSeek-V4-Pro', apiKey: 'sk-...', baseUrl: 'https://x' } as any,
45
- };
46
- s.chatAgent = 'forti-api';
47
- const mutated = migrateAgentsFlatten(s);
48
- if (!mutated) fail('expected mutated=true');
49
- if (s.agents['forti-api']) fail('forti-api still in agents');
50
- else pass('forti-api removed from agents');
51
- const p = (s as any).apiProfiles?.['forti-api'];
52
- if (!p) fail('forti-api not in apiProfiles');
53
- else if (p.provider !== 'openai-compatible') fail(`provider mapped wrong: ${p.provider}`);
54
- else if (p.model !== 'DeepSeek-V4-Pro') fail('model lost');
55
- else pass(`forti-api in apiProfiles (provider=${p.provider}, model=${p.model})`);
56
- if (s.chatAgent !== 'forti-api') fail(`chatAgent corrupted: ${s.chatAgent}`);
57
- else pass('chatAgent unchanged');
58
- }
59
-
60
- // ── Test 2: base/cliType → tool flattening ──────────────────────────
61
- {
62
- console.log('Test 2 — CLI profile flattens to tool field');
63
- const s = baseSettings();
64
- s.agents = {
65
- claude: { enabled: true, path: '/usr/local/bin/claude' },
66
- 'forti-coder': { base: 'claude', model: 'sonnet', env: { ANTHROPIC_AUTH_TOKEN: 'tok' } } as any,
67
- 'codex-dev': { cliType: 'codex', env: { OPENAI_API_KEY: 'k' } } as any,
68
- };
69
- migrateAgentsFlatten(s);
70
- if (s.agents['claude']?.tool !== 'claude') fail('claude builtin tool not inferred');
71
- else pass('claude tool inferred from id');
72
- if (s.agents['forti-coder']?.tool !== 'claude') fail(`forti-coder tool wrong: ${s.agents['forti-coder']?.tool}`);
73
- else pass('forti-coder tool inferred from base');
74
- if ((s.agents['forti-coder'] as any).base) fail('base field not cleaned');
75
- else pass('base field removed');
76
- if (s.agents['codex-dev']?.tool !== 'codex') fail(`codex-dev tool wrong: ${s.agents['codex-dev']?.tool}`);
77
- else pass('codex-dev tool inferred from cliType');
78
- }
79
-
80
- // ── Test 3: defaultAgent pointing to API profile downgrades ──────────
81
- {
82
- console.log('Test 3 — defaultAgent → apiProfile downgrades to claude');
83
- const s = baseSettings();
84
- s.agents = {
85
- claude: { enabled: true, path: '' },
86
- 'forti-api': { type: 'api', provider: 'anthropic', model: 'm', apiKey: 'k' } as any,
87
- };
88
- s.defaultAgent = 'forti-api'; // wrong! API id as task default
89
- migrateAgentsFlatten(s);
90
- if (s.defaultAgent !== 'claude') fail(`expected 'claude', got '${s.defaultAgent}'`);
91
- else pass('defaultAgent downgraded to claude');
92
- }
93
-
94
- // ── Test 4: chatAgent pointing to CLI downgrades ─────────────────────
95
- {
96
- console.log('Test 4 — chatAgent → CLI agent downgrades to first apiProfile');
97
- const s = baseSettings();
98
- s.agents = {
99
- claude: { enabled: true, path: '' },
100
- 'forti-coder': { base: 'claude', model: 'sonnet' } as any,
101
- };
102
- (s as any).apiProfiles = {
103
- 'real-api': { provider: 'anthropic', model: 'claude-sonnet-4-6', apiKey: 'k', enabled: true },
104
- };
105
- s.chatAgent = 'forti-coder'; // wrong! CLI as chat default
106
- migrateAgentsFlatten(s);
107
- if (s.chatAgent !== 'real-api') fail(`expected 'real-api', got '${s.chatAgent}'`);
108
- else pass('chatAgent downgraded to real-api');
109
- }
110
-
111
- // ── Test 5: idempotent on already-migrated shape ────────────────────
112
- {
113
- console.log('Test 5 — idempotent');
114
- const s = baseSettings();
115
- s.agents = {
116
- claude: { tool: 'claude', enabled: true, path: '' },
117
- 'forti-coder': { tool: 'claude', enabled: true, model: 'sonnet', env: {} },
118
- };
119
- (s as any).apiProfiles = {
120
- 'forti-api': { provider: 'openai-compatible', model: 'X', apiKey: 'k', enabled: true },
121
- };
122
- s.defaultAgent = 'claude';
123
- s.chatAgent = 'forti-api';
124
- const mutated = migrateAgentsFlatten(s);
125
- if (mutated) fail('expected no mutation on already-migrated input');
126
- else pass('idempotent (no-op)');
127
- }
128
-
129
- // ── Test 6: orphan entry (no inference possible) is left alone ──────
130
- {
131
- console.log('Test 6 — orphan entry preserved');
132
- const s = baseSettings();
133
- s.agents = {
134
- 'mystery-thing': { enabled: true, model: 'whatever' } as any,
135
- };
136
- migrateAgentsFlatten(s);
137
- if (!s.agents['mystery-thing']) fail('mystery-thing got deleted');
138
- else if ((s.agents['mystery-thing'] as any).tool) fail('mystery-thing tool inferred when it shouldn\'t');
139
- else pass('orphan preserved without tool (UI will skip, user re-adds)');
140
- }
141
-
142
- console.log('');
143
- if (failures === 0) {
144
- console.log('✓ All migration smoke checks passed');
145
- process.exit(0);
146
- } else {
147
- console.log(`✗ ${failures} check(s) failed`);
148
- process.exit(1);
149
- }