@aion0/forge 0.10.12 → 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.
- package/RELEASE_NOTES.md +6 -3
- package/app/api/public-info/[resource]/route.ts +40 -0
- package/components/SettingsModal.tsx +42 -33
- package/components/WorkspaceView.tsx +5 -3
- package/lib/agents/known-models.ts +75 -0
- package/lib/public-info/fetch.ts +116 -0
- package/lib/public-info/types.ts +38 -0
- package/lib/public-info/use-models-registry.ts +66 -0
- package/lib/settings.ts +9 -0
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
- package/lib/__tests__/foreach-batch-yaml.test.ts +0 -33
- package/lib/__tests__/foreach-before.test.ts +0 -201
- package/lib/__tests__/foreach-parse.test.ts +0 -114
- package/lib/__tests__/foreach-snapshot.test.ts +0 -112
- package/lib/__tests__/foreach-source.test.ts +0 -105
- package/lib/__tests__/foreach-template.test.ts +0 -112
- package/lib/workspace/__tests__/state-machine.test.ts +0 -388
- package/lib/workspace/__tests__/workspace.test.ts +0 -311
- package/scripts/bench/README.md +0 -66
- package/scripts/bench/results/.gitignore +0 -2
- package/scripts/bench/run.ts +0 -635
- package/scripts/bench/tasks/01-text-utils/task.md +0 -26
- package/scripts/bench/tasks/01-text-utils/validator.sh +0 -46
- package/scripts/bench/tasks/02-pagination/setup.sh +0 -19
- package/scripts/bench/tasks/02-pagination/task.md +0 -48
- package/scripts/bench/tasks/02-pagination/validator.sh +0 -69
- package/scripts/bench/tasks/03-bug-fix/setup.sh +0 -82
- package/scripts/bench/tasks/03-bug-fix/task.md +0 -30
- package/scripts/bench/tasks/03-bug-fix/validator.sh +0 -29
- package/scripts/test-agents-migrate.ts +0 -149
- package/scripts/test-mantis.ts +0 -223
- package/scripts/test-memory-local.ts +0 -139
- package/scripts/test-memory-upsert.ts +0 -106
- 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: '',
|
|
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, models: { task: '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
|
-
}
|