@crouton-kit/crouter 0.3.11 → 0.3.12
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/bin/crtrd +2 -0
- package/dist/builtin-personas/design/base.md +9 -0
- package/dist/builtin-personas/design/orchestrator.md +10 -0
- package/dist/builtin-personas/developer/base.md +9 -0
- package/dist/builtin-personas/developer/orchestrator.md +12 -0
- package/dist/builtin-personas/explore/base.md +9 -0
- package/dist/builtin-personas/explore/orchestrator.md +9 -0
- package/dist/builtin-personas/general/base.md +5 -0
- package/dist/builtin-personas/general/orchestrator.md +7 -0
- package/dist/builtin-personas/orchestration-kernel.md +71 -0
- package/dist/builtin-personas/plan/base.md +7 -0
- package/dist/builtin-personas/plan/orchestrator.md +12 -0
- package/dist/builtin-personas/review/base.md +7 -0
- package/dist/builtin-personas/review/orchestrator.md +9 -0
- package/dist/builtin-personas/runtime-base.md +39 -0
- package/dist/builtin-personas/spec/base.md +7 -0
- package/dist/builtin-personas/spec/orchestrator.md +10 -0
- package/dist/builtin-skills/skills/design/SKILL.md +51 -0
- package/dist/builtin-skills/skills/development/SKILL.md +109 -0
- package/dist/builtin-skills/skills/planning/SKILL.md +59 -0
- package/dist/builtin-skills/skills/spec/SKILL.md +83 -0
- package/dist/cli.js +14 -6
- package/dist/commands/{mode.d.ts → attention.d.ts} +1 -1
- package/dist/commands/attention.js +152 -0
- package/dist/commands/canvas.d.ts +2 -0
- package/dist/commands/canvas.js +35 -0
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +111 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +65 -0
- package/dist/commands/human/prompts.d.ts +5 -0
- package/dist/commands/human/prompts.js +269 -0
- package/dist/commands/human/queue.d.ts +3 -0
- package/dist/commands/human/queue.js +133 -0
- package/dist/commands/human/shared.d.ts +43 -0
- package/dist/commands/human/shared.js +107 -0
- package/dist/commands/human.js +10 -454
- package/dist/commands/node.d.ts +2 -0
- package/dist/commands/node.js +354 -0
- package/dist/commands/pkg/market-inspect.d.ts +1 -0
- package/dist/commands/pkg/market-inspect.js +157 -0
- package/dist/commands/pkg/market-manage.d.ts +1 -0
- package/dist/commands/pkg/market-manage.js +316 -0
- package/dist/commands/pkg/market.d.ts +1 -0
- package/dist/commands/pkg/market.js +16 -0
- package/dist/commands/pkg/plugin-inspect.d.ts +1 -0
- package/dist/commands/pkg/plugin-inspect.js +142 -0
- package/dist/commands/pkg/plugin-manage.d.ts +1 -0
- package/dist/commands/pkg/plugin-manage.js +294 -0
- package/dist/commands/pkg/plugin.d.ts +1 -0
- package/dist/commands/pkg/plugin.js +16 -0
- package/dist/commands/pkg/shared.d.ts +5 -0
- package/dist/commands/pkg/shared.js +61 -0
- package/dist/commands/pkg.js +3 -1004
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.js +159 -0
- package/dist/commands/revive.d.ts +2 -0
- package/dist/commands/revive.js +64 -0
- package/dist/commands/skill/author.d.ts +3 -0
- package/dist/commands/skill/author.js +147 -0
- package/dist/commands/skill/find.d.ts +4 -0
- package/dist/commands/skill/find.js +254 -0
- package/dist/commands/skill/read.d.ts +1 -0
- package/dist/commands/skill/read.js +89 -0
- package/dist/commands/skill/shared.d.ts +19 -0
- package/dist/commands/skill/shared.js +207 -0
- package/dist/commands/skill/state.d.ts +3 -0
- package/dist/commands/skill/state.js +69 -0
- package/dist/commands/skill.js +6 -691
- package/dist/commands/sys/config.d.ts +1 -0
- package/dist/commands/sys/config.js +186 -0
- package/dist/commands/sys/doctor.d.ts +1 -0
- package/dist/commands/sys/doctor.js +369 -0
- package/dist/commands/sys/shared.d.ts +3 -0
- package/dist/commands/sys/shared.js +24 -0
- package/dist/commands/sys/update.d.ts +2 -0
- package/dist/commands/sys/update.js +114 -0
- package/dist/commands/sys.js +4 -694
- package/dist/core/__tests__/argv-parser.test.js +19 -1
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +100 -0
- package/dist/core/__tests__/canvas.test.js +154 -0
- package/dist/core/__tests__/reset.test.js +105 -0
- package/dist/core/canvas/attention.d.ts +24 -0
- package/dist/core/canvas/attention.js +94 -0
- package/dist/core/canvas/canvas.d.ts +40 -0
- package/dist/core/canvas/canvas.js +210 -0
- package/dist/core/canvas/db.d.ts +7 -0
- package/dist/core/canvas/db.js +61 -0
- package/dist/core/canvas/index.d.ts +4 -0
- package/dist/core/canvas/index.js +6 -0
- package/dist/core/canvas/paths.d.ts +16 -0
- package/dist/core/canvas/paths.js +62 -0
- package/dist/core/canvas/render.d.ts +30 -0
- package/dist/core/canvas/render.js +186 -0
- package/dist/core/canvas/types.d.ts +87 -0
- package/dist/core/canvas/types.js +8 -0
- package/dist/core/command.d.ts +5 -0
- package/dist/core/command.js +35 -10
- package/dist/core/feed/feed.d.ts +43 -0
- package/dist/core/feed/feed.js +116 -0
- package/dist/core/feed/inbox.d.ts +50 -0
- package/dist/core/feed/inbox.js +124 -0
- package/dist/core/help.js +5 -3
- package/dist/core/io.d.ts +15 -1
- package/dist/core/io.js +56 -6
- package/dist/core/personas/index.d.ts +12 -0
- package/dist/core/personas/index.js +10 -0
- package/dist/core/personas/loader.d.ts +44 -0
- package/dist/core/personas/loader.js +157 -0
- package/dist/core/personas/resolve.d.ts +36 -0
- package/dist/core/personas/resolve.js +110 -0
- package/dist/core/render.d.ts +11 -0
- package/dist/core/render.js +126 -0
- package/dist/core/resolver.d.ts +10 -0
- package/dist/core/resolver.js +109 -1
- package/dist/core/runtime/front-door.d.ts +10 -0
- package/dist/core/runtime/front-door.js +97 -0
- package/dist/core/runtime/kickoff.d.ts +23 -0
- package/dist/core/runtime/kickoff.js +134 -0
- package/dist/core/runtime/launch.d.ts +34 -0
- package/dist/core/runtime/launch.js +85 -0
- package/dist/core/runtime/nodes.d.ts +38 -0
- package/dist/core/runtime/nodes.js +95 -0
- package/dist/core/runtime/presence.d.ts +38 -0
- package/dist/core/runtime/presence.js +152 -0
- package/dist/core/runtime/promote.d.ts +30 -0
- package/dist/core/runtime/promote.js +105 -0
- package/dist/core/runtime/reset.d.ts +13 -0
- package/dist/core/runtime/reset.js +97 -0
- package/dist/core/runtime/revive.d.ts +26 -0
- package/dist/core/runtime/revive.js +89 -0
- package/dist/core/runtime/roadmap.d.ts +12 -0
- package/dist/core/runtime/roadmap.js +52 -0
- package/dist/core/runtime/spawn.d.ts +33 -0
- package/dist/core/runtime/spawn.js +118 -0
- package/dist/core/runtime/stop-guard.d.ts +18 -0
- package/dist/core/runtime/stop-guard.js +33 -0
- package/dist/core/runtime/tmux.d.ts +88 -0
- package/dist/core/runtime/tmux.js +198 -0
- package/dist/core/spawn.d.ts +17 -197
- package/dist/core/spawn.js +16 -539
- package/dist/daemon/crtrd-cli.js +4 -0
- package/dist/daemon/crtrd.d.ts +20 -0
- package/dist/daemon/crtrd.js +200 -0
- package/dist/daemon/manage.d.ts +17 -0
- package/dist/daemon/manage.js +57 -0
- package/dist/pi-extensions/canvas-inbox-watcher.d.ts +16 -0
- package/dist/pi-extensions/canvas-inbox-watcher.js +229 -0
- package/dist/pi-extensions/canvas-nav.d.ts +32 -0
- package/dist/pi-extensions/canvas-nav.js +536 -0
- package/dist/pi-extensions/canvas-stophook.d.ts +17 -0
- package/dist/pi-extensions/canvas-stophook.js +373 -0
- package/package.json +6 -5
- package/dist/commands/agent.d.ts +0 -6
- package/dist/commands/agent.js +0 -585
- package/dist/commands/debug.d.ts +0 -3
- package/dist/commands/debug.js +0 -192
- package/dist/commands/job.d.ts +0 -11
- package/dist/commands/job.js +0 -384
- package/dist/commands/mode.js +0 -231
- package/dist/commands/plan.d.ts +0 -4
- package/dist/commands/plan.js +0 -322
- package/dist/commands/spec.d.ts +0 -3
- package/dist/commands/spec.js +0 -299
- package/dist/core/__tests__/flow-leaves.test.js +0 -248
- package/dist/core/__tests__/job.test.js +0 -310
- package/dist/core/__tests__/jobs.test.js +0 -98
- package/dist/core/__tests__/spawn.test.js +0 -138
- package/dist/core/__tests__/subagents.test.d.ts +0 -1
- package/dist/core/__tests__/subagents.test.js +0 -75
- package/dist/core/jobs.d.ts +0 -107
- package/dist/core/jobs.js +0 -565
- package/dist/core/subagents.d.ts +0 -18
- package/dist/core/subagents.js +0 -163
- package/dist/prompts/agent.d.ts +0 -27
- package/dist/prompts/agent.js +0 -184
- package/dist/prompts/debug.d.ts +0 -8
- package/dist/prompts/debug.js +0 -44
- /package/dist/core/__tests__/{flow-leaves.test.d.ts → canvas-inbox-watcher.test.d.ts} +0 -0
- /package/dist/core/__tests__/{job.test.d.ts → canvas.test.d.ts} +0 -0
- /package/dist/core/__tests__/{jobs.test.d.ts → reset.test.d.ts} +0 -0
- /package/dist/{core/__tests__/spawn.test.d.ts → daemon/crtrd-cli.d.ts} +0 -0
|
@@ -1,310 +0,0 @@
|
|
|
1
|
-
// Tests for the job subtree (monitoring) and agent/mode spawn leaves argv
|
|
2
|
-
// migration. Exercises parseArgv against each leaf's param schema directly —
|
|
3
|
-
// no subprocess, no tmux, no filesystem side-effects from the handler.
|
|
4
|
-
//
|
|
5
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/job.test.ts
|
|
6
|
-
import { test, describe } from 'node:test';
|
|
7
|
-
import assert from 'node:assert/strict';
|
|
8
|
-
import { parseArgv } from '../command.js';
|
|
9
|
-
// ---------------------------------------------------------------------------
|
|
10
|
-
// Param schemas extracted from each leaf (mirrors job.ts exactly)
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
const startPromptParams = [
|
|
13
|
-
{ kind: 'stdin', name: 'prompt', required: true, constraint: '' },
|
|
14
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
15
|
-
];
|
|
16
|
-
const startForkParams = [
|
|
17
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
18
|
-
];
|
|
19
|
-
const startPlannerParams = [
|
|
20
|
-
{ kind: 'positional', name: 'spec_path', type: 'path', required: true, constraint: '' },
|
|
21
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
22
|
-
];
|
|
23
|
-
const startImplementerParams = [
|
|
24
|
-
{ kind: 'positional', name: 'plan_path', type: 'path', required: true, constraint: '' },
|
|
25
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
26
|
-
];
|
|
27
|
-
const startReviewerParams = [
|
|
28
|
-
{ kind: 'positional', name: 'artifact_path', type: 'path', required: true, constraint: '' },
|
|
29
|
-
{ kind: 'flag', name: 'kind', type: 'enum', choices: ['plan', 'spec'], required: true, constraint: '' },
|
|
30
|
-
{ kind: 'flag', name: 'spec-path', type: 'path', required: false, constraint: '' },
|
|
31
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
32
|
-
];
|
|
33
|
-
const readListParams = [
|
|
34
|
-
{ kind: 'flag', name: 'limit', type: 'int', required: false, default: 20, constraint: '' },
|
|
35
|
-
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: '' },
|
|
36
|
-
];
|
|
37
|
-
const readStatusParams = [
|
|
38
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
39
|
-
];
|
|
40
|
-
const readResultParams = [
|
|
41
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
42
|
-
{ kind: 'flag', name: 'wait', type: 'bool', required: false, constraint: '' },
|
|
43
|
-
];
|
|
44
|
-
const readLogsParams = [
|
|
45
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
46
|
-
{ kind: 'flag', name: 'since', type: 'string', required: false, constraint: '' },
|
|
47
|
-
{ kind: 'flag', name: 'until', type: 'string', required: false, constraint: '' },
|
|
48
|
-
{ kind: 'flag', name: 'level', type: 'enum', choices: ['debug', 'info', 'warn', 'error'], required: false, default: 'info', constraint: '' },
|
|
49
|
-
{ kind: 'flag', name: 'follow', type: 'bool', required: false, constraint: '' },
|
|
50
|
-
];
|
|
51
|
-
// NOTE: the real leaf also declares a `stdin` param for `body`. We omit it
|
|
52
|
-
// from the test schema because parseArgv reads stdin to EOF whenever a stdin
|
|
53
|
-
// param is declared — and under `node --test`, stdin is piped with no EOF, so
|
|
54
|
-
// the call hangs forever. The body-required-on-status=done check lives in the
|
|
55
|
-
// leaf's `run` handler, not in parseArgv, so the schema tests below cover
|
|
56
|
-
// everything parseArgv can see without needing stdin.
|
|
57
|
-
const submitParams = [
|
|
58
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
59
|
-
{ kind: 'flag', name: 'status', type: 'enum', choices: ['done', 'failed'], required: false, default: 'done', constraint: '' },
|
|
60
|
-
{ kind: 'flag', name: 'reason', type: 'string', required: false, constraint: '' },
|
|
61
|
-
{ kind: 'flag', name: 'kill-pane', type: 'bool', required: false, constraint: '' },
|
|
62
|
-
];
|
|
63
|
-
const cancelParams = [
|
|
64
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
65
|
-
];
|
|
66
|
-
const failParams = [
|
|
67
|
-
{ kind: 'positional', name: 'job_id', type: 'string', required: true, constraint: '' },
|
|
68
|
-
];
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// agent new (formerly job start prompt)
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
describe('agent new', () => {
|
|
73
|
-
// stdin is handled by readStdinRaw() which requires actual stdin — we only
|
|
74
|
-
// test the non-stdin flag parsing here.
|
|
75
|
-
test('--cwd flag parsed as string', async () => {
|
|
76
|
-
// stdin param will be read but we can't pipe here — skip stdin assertion,
|
|
77
|
-
// test that cwd parses correctly alongside other tokens.
|
|
78
|
-
// We test the flag-only shape: no stdin params in isolation.
|
|
79
|
-
const flagOnlyParams = [
|
|
80
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
81
|
-
];
|
|
82
|
-
const result = await parseArgv(flagOnlyParams, ['--cwd', '/tmp/mydir']);
|
|
83
|
-
assert.equal(result['cwd'], '/tmp/mydir');
|
|
84
|
-
});
|
|
85
|
-
test('cwd absent → undefined', async () => {
|
|
86
|
-
const flagOnlyParams = [
|
|
87
|
-
{ kind: 'flag', name: 'cwd', type: 'path', required: false, constraint: '' },
|
|
88
|
-
];
|
|
89
|
-
const result = await parseArgv(flagOnlyParams, []);
|
|
90
|
-
assert.equal(result['cwd'], undefined);
|
|
91
|
-
});
|
|
92
|
-
});
|
|
93
|
-
// ---------------------------------------------------------------------------
|
|
94
|
-
// agent fork (formerly job start fork)
|
|
95
|
-
// ---------------------------------------------------------------------------
|
|
96
|
-
describe('agent fork', () => {
|
|
97
|
-
test('no args parses cleanly', async () => {
|
|
98
|
-
const result = await parseArgv(startForkParams, []);
|
|
99
|
-
assert.equal(result['cwd'], undefined);
|
|
100
|
-
});
|
|
101
|
-
test('--cwd parsed', async () => {
|
|
102
|
-
const result = await parseArgv(startForkParams, ['--cwd', '/workspace']);
|
|
103
|
-
assert.equal(result['cwd'], '/workspace');
|
|
104
|
-
});
|
|
105
|
-
test('unknown positional rejected', async () => {
|
|
106
|
-
await assert.rejects(() => parseArgv(startForkParams, ['extra-pos']), (err) => { assert.match(err.message, /takes no positional/); return true; });
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
// mode planner (formerly job start planner)
|
|
111
|
-
// ---------------------------------------------------------------------------
|
|
112
|
-
describe('mode planner', () => {
|
|
113
|
-
test('positional spec_path required', async () => {
|
|
114
|
-
await assert.rejects(() => parseArgv(startPlannerParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
115
|
-
});
|
|
116
|
-
test('positional parsed as spec_path (camelCase: specPath? no — underscore stays as-is)', async () => {
|
|
117
|
-
const result = await parseArgv(startPlannerParams, ['/tmp/spec.md']);
|
|
118
|
-
// flagNameToKey converts kebab to camel; underscores are unaffected
|
|
119
|
-
assert.equal(result['spec_path'], '/tmp/spec.md');
|
|
120
|
-
});
|
|
121
|
-
test('--cwd optional', async () => {
|
|
122
|
-
const result = await parseArgv(startPlannerParams, ['/tmp/spec.md', '--cwd', '/src']);
|
|
123
|
-
assert.equal(result['cwd'], '/src');
|
|
124
|
-
assert.equal(result['spec_path'], '/tmp/spec.md');
|
|
125
|
-
});
|
|
126
|
-
});
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// mode implementer (formerly job start implementer)
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
describe('mode implementer', () => {
|
|
131
|
-
test('positional plan_path required', async () => {
|
|
132
|
-
await assert.rejects(() => parseArgv(startImplementerParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
133
|
-
});
|
|
134
|
-
test('positional parsed', async () => {
|
|
135
|
-
const result = await parseArgv(startImplementerParams, ['/tmp/plan.md']);
|
|
136
|
-
assert.equal(result['plan_path'], '/tmp/plan.md');
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
// ---------------------------------------------------------------------------
|
|
140
|
-
// mode reviewer (formerly job start reviewer)
|
|
141
|
-
// ---------------------------------------------------------------------------
|
|
142
|
-
describe('mode reviewer', () => {
|
|
143
|
-
test('positional + --kind required', async () => {
|
|
144
|
-
await assert.rejects(() => parseArgv(startReviewerParams, ['/tmp/artifact.md']), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
145
|
-
});
|
|
146
|
-
test('valid kind: plan', async () => {
|
|
147
|
-
const result = await parseArgv(startReviewerParams, ['/tmp/artifact.md', '--kind', 'plan']);
|
|
148
|
-
assert.equal(result['artifact_path'], '/tmp/artifact.md');
|
|
149
|
-
assert.equal(result['kind'], 'plan');
|
|
150
|
-
});
|
|
151
|
-
test('valid kind: spec', async () => {
|
|
152
|
-
const result = await parseArgv(startReviewerParams, ['/tmp/artifact.md', '--kind', 'spec']);
|
|
153
|
-
assert.equal(result['kind'], 'spec');
|
|
154
|
-
});
|
|
155
|
-
test('invalid kind throws invalid_type', async () => {
|
|
156
|
-
await assert.rejects(() => parseArgv(startReviewerParams, ['/tmp/artifact.md', '--kind', 'bad']), (err) => { assert.match(err.message, /must be one of/); return true; });
|
|
157
|
-
});
|
|
158
|
-
test('--spec-path optional, becomes specPath', async () => {
|
|
159
|
-
const result = await parseArgv(startReviewerParams, [
|
|
160
|
-
'/tmp/artifact.md', '--kind', 'plan', '--spec-path', '/tmp/spec.md',
|
|
161
|
-
]);
|
|
162
|
-
assert.equal(result['specPath'], '/tmp/spec.md');
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// job read list
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
describe('job read list', () => {
|
|
169
|
-
test('defaults: limit=20, cursor=undefined', async () => {
|
|
170
|
-
const result = await parseArgv(readListParams, []);
|
|
171
|
-
assert.equal(result['limit'], 20);
|
|
172
|
-
assert.equal(result['cursor'], undefined);
|
|
173
|
-
});
|
|
174
|
-
test('--limit N parsed as int', async () => {
|
|
175
|
-
const result = await parseArgv(readListParams, ['--limit', '50']);
|
|
176
|
-
assert.equal(result['limit'], 50);
|
|
177
|
-
});
|
|
178
|
-
test('--limit with non-integer throws', async () => {
|
|
179
|
-
await assert.rejects(() => parseArgv(readListParams, ['--limit', 'abc']), (err) => { assert.match(err.message, /must be an integer/); return true; });
|
|
180
|
-
});
|
|
181
|
-
test('--cursor opaque token', async () => {
|
|
182
|
-
const result = await parseArgv(readListParams, ['--cursor', 'tok_abc123']);
|
|
183
|
-
assert.equal(result['cursor'], 'tok_abc123');
|
|
184
|
-
});
|
|
185
|
-
test('--limit and --cursor together', async () => {
|
|
186
|
-
const result = await parseArgv(readListParams, ['--limit', '5', '--cursor', 'next_page']);
|
|
187
|
-
assert.equal(result['limit'], 5);
|
|
188
|
-
assert.equal(result['cursor'], 'next_page');
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// job read status
|
|
193
|
-
// ---------------------------------------------------------------------------
|
|
194
|
-
describe('job read status', () => {
|
|
195
|
-
test('positional job_id required', async () => {
|
|
196
|
-
await assert.rejects(() => parseArgv(readStatusParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
197
|
-
});
|
|
198
|
-
test('positional job_id parsed', async () => {
|
|
199
|
-
const result = await parseArgv(readStatusParams, ['job-abc-123']);
|
|
200
|
-
assert.equal(result['job_id'], 'job-abc-123');
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
// ---------------------------------------------------------------------------
|
|
204
|
-
// job read result
|
|
205
|
-
// ---------------------------------------------------------------------------
|
|
206
|
-
describe('job read result', () => {
|
|
207
|
-
test('positional job_id required', async () => {
|
|
208
|
-
await assert.rejects(() => parseArgv(readResultParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
209
|
-
});
|
|
210
|
-
test('positional job_id without --wait', async () => {
|
|
211
|
-
const result = await parseArgv(readResultParams, ['job-xyz']);
|
|
212
|
-
assert.equal(result['job_id'], 'job-xyz');
|
|
213
|
-
assert.equal(result['wait'], false);
|
|
214
|
-
});
|
|
215
|
-
test('--wait presence = true', async () => {
|
|
216
|
-
const result = await parseArgv(readResultParams, ['job-xyz', '--wait']);
|
|
217
|
-
assert.equal(result['wait'], true);
|
|
218
|
-
});
|
|
219
|
-
test('--wait=value rejected (bool takes no value)', async () => {
|
|
220
|
-
await assert.rejects(() => parseArgv(readResultParams, ['job-xyz', '--wait=true']), (err) => { assert.match(err.message, /takes no value/); return true; });
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
// ---------------------------------------------------------------------------
|
|
224
|
-
// job read logs
|
|
225
|
-
// ---------------------------------------------------------------------------
|
|
226
|
-
describe('job read logs', () => {
|
|
227
|
-
test('positional job_id required', async () => {
|
|
228
|
-
await assert.rejects(() => parseArgv(readLogsParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
229
|
-
});
|
|
230
|
-
test('defaults: level=info, follow=false', async () => {
|
|
231
|
-
const result = await parseArgv(readLogsParams, ['job-abc']);
|
|
232
|
-
assert.equal(result['job_id'], 'job-abc');
|
|
233
|
-
assert.equal(result['level'], 'info');
|
|
234
|
-
assert.equal(result['follow'], false);
|
|
235
|
-
});
|
|
236
|
-
test('--follow presence = true', async () => {
|
|
237
|
-
const result = await parseArgv(readLogsParams, ['job-abc', '--follow']);
|
|
238
|
-
assert.equal(result['follow'], true);
|
|
239
|
-
});
|
|
240
|
-
test('--follow=value rejected', async () => {
|
|
241
|
-
await assert.rejects(() => parseArgv(readLogsParams, ['job-abc', '--follow=true']), (err) => { assert.match(err.message, /takes no value/); return true; });
|
|
242
|
-
});
|
|
243
|
-
test('--level valid enum', async () => {
|
|
244
|
-
const result = await parseArgv(readLogsParams, ['job-abc', '--level', 'debug']);
|
|
245
|
-
assert.equal(result['level'], 'debug');
|
|
246
|
-
});
|
|
247
|
-
test('--level invalid enum throws', async () => {
|
|
248
|
-
await assert.rejects(() => parseArgv(readLogsParams, ['job-abc', '--level', 'trace']), (err) => { assert.match(err.message, /must be one of/); return true; });
|
|
249
|
-
});
|
|
250
|
-
test('--since and --until optional strings', async () => {
|
|
251
|
-
const result = await parseArgv(readLogsParams, [
|
|
252
|
-
'job-abc', '--since', '2025-01-01T00:00:00Z', '--until', '2025-01-02T00:00:00Z',
|
|
253
|
-
]);
|
|
254
|
-
assert.equal(result['since'], '2025-01-01T00:00:00Z');
|
|
255
|
-
assert.equal(result['until'], '2025-01-02T00:00:00Z');
|
|
256
|
-
});
|
|
257
|
-
});
|
|
258
|
-
// ---------------------------------------------------------------------------
|
|
259
|
-
// job submit
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
describe('job submit', () => {
|
|
262
|
-
test('positional job_id + defaults: status=done, killPane=false', async () => {
|
|
263
|
-
const result = await parseArgv(submitParams, ['job-abc']);
|
|
264
|
-
assert.equal(result['job_id'], 'job-abc');
|
|
265
|
-
assert.equal(result['status'], 'done');
|
|
266
|
-
assert.equal(result['killPane'], false);
|
|
267
|
-
});
|
|
268
|
-
test('--status failed parsed', async () => {
|
|
269
|
-
const result = await parseArgv(submitParams, ['job-abc', '--status', 'failed', '--reason', 'broken']);
|
|
270
|
-
assert.equal(result['status'], 'failed');
|
|
271
|
-
assert.equal(result['reason'], 'broken');
|
|
272
|
-
});
|
|
273
|
-
test('--status invalid enum throws', async () => {
|
|
274
|
-
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--status', 'bogus']), (err) => { assert.match(err.message, /must be one of/); return true; });
|
|
275
|
-
});
|
|
276
|
-
test('--kill-pane presence = true, killPane key', async () => {
|
|
277
|
-
const result = await parseArgv(submitParams, ['job-abc', '--kill-pane']);
|
|
278
|
-
assert.equal(result['killPane'], true);
|
|
279
|
-
});
|
|
280
|
-
test('missing positional job_id throws missing_parameter', async () => {
|
|
281
|
-
await assert.rejects(() => parseArgv(submitParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
282
|
-
});
|
|
283
|
-
test('--context-file no longer accepted', async () => {
|
|
284
|
-
await assert.rejects(() => parseArgv(submitParams, ['job-abc', '--context-file', '/tmp/anything']), (err) => { assert.match(err.message, /unknown flag: --context-file/); return true; });
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// job cancel
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
describe('job cancel', () => {
|
|
291
|
-
test('positional job_id required', async () => {
|
|
292
|
-
await assert.rejects(() => parseArgv(cancelParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
293
|
-
});
|
|
294
|
-
test('positional job_id parsed', async () => {
|
|
295
|
-
const result = await parseArgv(cancelParams, ['job-abc-123']);
|
|
296
|
-
assert.equal(result['job_id'], 'job-abc-123');
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
// ---------------------------------------------------------------------------
|
|
300
|
-
// job _fail
|
|
301
|
-
// ---------------------------------------------------------------------------
|
|
302
|
-
describe('job _fail', () => {
|
|
303
|
-
test('positional job_id required', async () => {
|
|
304
|
-
await assert.rejects(() => parseArgv(failParams, []), (err) => { assert.match(err.message, /required parameter is missing/); return true; });
|
|
305
|
-
});
|
|
306
|
-
test('positional job_id parsed', async () => {
|
|
307
|
-
const result = await parseArgv(failParams, ['job-fail-001']);
|
|
308
|
-
assert.equal(result['job_id'], 'job-fail-001');
|
|
309
|
-
});
|
|
310
|
-
});
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
// Tests for jobs.ts result-file storage (markdown vs json paths).
|
|
2
|
-
//
|
|
3
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/jobs.test.ts
|
|
4
|
-
import { test, describe, before, after } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { createJob, writeResult, writeMarkdownResult, readResult, recordJobPane, jobStatus, } from '../jobs.js';
|
|
10
|
-
let stateDir;
|
|
11
|
-
let origXdg;
|
|
12
|
-
before(() => {
|
|
13
|
-
stateDir = join(tmpdir(), `crtr-jobs-test-${Date.now()}`);
|
|
14
|
-
mkdirSync(stateDir, { recursive: true });
|
|
15
|
-
origXdg = process.env['XDG_STATE_HOME'];
|
|
16
|
-
process.env['XDG_STATE_HOME'] = stateDir;
|
|
17
|
-
});
|
|
18
|
-
after(() => {
|
|
19
|
-
if (origXdg === undefined) {
|
|
20
|
-
delete process.env['XDG_STATE_HOME'];
|
|
21
|
-
}
|
|
22
|
-
else {
|
|
23
|
-
process.env['XDG_STATE_HOME'] = origXdg;
|
|
24
|
-
}
|
|
25
|
-
rmSync(stateDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
describe('writeMarkdownResult + readResult round-trip', () => {
|
|
28
|
-
test('done with body writes result.md, parses frontmatter back', async () => {
|
|
29
|
-
const { jobId, dir } = createJob('prompt', { cwd: '/tmp' });
|
|
30
|
-
const body = '**Summary:** all good\n\nMore details on the next line.\n';
|
|
31
|
-
writeMarkdownResult(jobId, body, 'done');
|
|
32
|
-
assert.ok(existsSync(join(dir, 'result.md')), 'result.md should exist');
|
|
33
|
-
assert.ok(!existsSync(join(dir, 'result.json')), 'result.json should NOT exist on md path');
|
|
34
|
-
const raw = readFileSync(join(dir, 'result.md'), 'utf8');
|
|
35
|
-
assert.match(raw, /^---\nstatus: done\nwritten_at: \d{4}-\d{2}-\d{2}T/);
|
|
36
|
-
assert.ok(raw.endsWith(body), 'body preserved at end of file');
|
|
37
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
38
|
-
assert.equal(r.status, 'done');
|
|
39
|
-
assert.equal(r.result_md, body);
|
|
40
|
-
assert.equal(r.reason, undefined);
|
|
41
|
-
assert.equal(r.result, undefined);
|
|
42
|
-
});
|
|
43
|
-
test('failed with reason writes reason into frontmatter and reads it back', async () => {
|
|
44
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
45
|
-
writeMarkdownResult(jobId, '', 'failed', 'broke: had "quoted" parts and a\nnewline');
|
|
46
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
47
|
-
assert.equal(r.status, 'failed');
|
|
48
|
-
assert.equal(r.result_md, '');
|
|
49
|
-
assert.equal(r.reason, 'broke: had "quoted" parts and a\nnewline');
|
|
50
|
-
});
|
|
51
|
-
test('writeResult (JSON) writes result.json and read still works', async () => {
|
|
52
|
-
const { jobId, dir } = createJob('prompt', { cwd: '/tmp' });
|
|
53
|
-
writeResult(jobId, { feedback: 'approved', n: 3 }, 'done');
|
|
54
|
-
assert.ok(existsSync(join(dir, 'result.json')));
|
|
55
|
-
assert.ok(!existsSync(join(dir, 'result.md')));
|
|
56
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
57
|
-
assert.equal(r.status, 'done');
|
|
58
|
-
assert.deepEqual(r.result, { feedback: 'approved', n: 3 });
|
|
59
|
-
assert.equal(r.result_md, undefined);
|
|
60
|
-
});
|
|
61
|
-
test('readResult with no result file and waitMs=0 returns timeout', async () => {
|
|
62
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
63
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
64
|
-
assert.equal(r.status, 'timeout');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
67
|
-
describe('closed-pane reaping (zombie prevention)', () => {
|
|
68
|
-
test('live job whose recorded pane is gone is reaped to closed on status read', () => {
|
|
69
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
70
|
-
// A pane id that cannot exist on any tmux server.
|
|
71
|
-
recordJobPane(jobId, '%999999999');
|
|
72
|
-
const status = jobStatus(jobId);
|
|
73
|
-
assert.equal(status.state, 'closed');
|
|
74
|
-
});
|
|
75
|
-
test('reaped job exposes a closed result explaining the closed pane', async () => {
|
|
76
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
77
|
-
recordJobPane(jobId, '%999999999');
|
|
78
|
-
// Trigger the reaper.
|
|
79
|
-
jobStatus(jobId);
|
|
80
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
81
|
-
assert.equal(r.status, 'closed');
|
|
82
|
-
assert.match(r.reason ?? '', /pane closed/);
|
|
83
|
-
});
|
|
84
|
-
test('job with no recorded pane is NOT reaped (stays live)', () => {
|
|
85
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
86
|
-
const status = jobStatus(jobId);
|
|
87
|
-
assert.equal(status.state, 'live');
|
|
88
|
-
});
|
|
89
|
-
test('already-submitted job is never overwritten by the reaper', async () => {
|
|
90
|
-
const { jobId } = createJob('prompt', { cwd: '/tmp' });
|
|
91
|
-
recordJobPane(jobId, '%999999999');
|
|
92
|
-
writeMarkdownResult(jobId, '**done**\n', 'done');
|
|
93
|
-
jobStatus(jobId);
|
|
94
|
-
const r = await readResult(jobId, { waitMs: 0 });
|
|
95
|
-
assert.equal(r.status, 'done');
|
|
96
|
-
assert.equal(r.result_md, '**done**\n');
|
|
97
|
-
});
|
|
98
|
-
});
|
|
@@ -1,138 +0,0 @@
|
|
|
1
|
-
// Tests for agent-CLI selection in spawn.ts (detectAgentKind + buildAgentCommand).
|
|
2
|
-
//
|
|
3
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/spawn.test.ts
|
|
4
|
-
import { test, describe, afterEach } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { detectAgentKind, buildAgentCommand, buildAgentPrintArgv, buildAgentPrintCommand, normalizeModelForKind, subagentSessionName, } from '../spawn.js';
|
|
7
|
-
const origPi = process.env['PI_CODING_AGENT'];
|
|
8
|
-
afterEach(() => {
|
|
9
|
-
if (origPi === undefined)
|
|
10
|
-
delete process.env['PI_CODING_AGENT'];
|
|
11
|
-
else
|
|
12
|
-
process.env['PI_CODING_AGENT'] = origPi;
|
|
13
|
-
});
|
|
14
|
-
describe('subagentSessionName', () => {
|
|
15
|
-
test('derives a deterministic, tmux-safe name from a pane id', () => {
|
|
16
|
-
assert.equal(subagentSessionName('%5'), 'crtr-agents-5');
|
|
17
|
-
assert.equal(subagentSessionName('%23'), 'crtr-agents-23');
|
|
18
|
-
});
|
|
19
|
-
test('is stable for the same pane id and distinct across panes', () => {
|
|
20
|
-
assert.equal(subagentSessionName('%7'), subagentSessionName('%7'));
|
|
21
|
-
assert.notEqual(subagentSessionName('%7'), subagentSessionName('%8'));
|
|
22
|
-
});
|
|
23
|
-
test('strips characters tmux would treat specially', () => {
|
|
24
|
-
assert.equal(subagentSessionName('%1.2'), 'crtr-agents-12');
|
|
25
|
-
assert.match(subagentSessionName('%99'), /^crtr-agents-[a-zA-Z0-9]+$/);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
describe('detectAgentKind', () => {
|
|
29
|
-
test('returns pi when PI_CODING_AGENT=true', () => {
|
|
30
|
-
process.env['PI_CODING_AGENT'] = 'true';
|
|
31
|
-
assert.equal(detectAgentKind(), 'pi');
|
|
32
|
-
});
|
|
33
|
-
test('defaults to claude when no signal is present', () => {
|
|
34
|
-
delete process.env['PI_CODING_AGENT'];
|
|
35
|
-
assert.equal(detectAgentKind(), 'claude');
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
describe('buildAgentCommand: claude', () => {
|
|
39
|
-
test('fresh prompt uses --dangerously-skip-permissions and quotes the prompt', () => {
|
|
40
|
-
const cmd = buildAgentCommand({ prompt: 'do the thing', name: 'worker-1' }, 'claude');
|
|
41
|
-
assert.equal(cmd, "claude -n 'worker-1' --dangerously-skip-permissions 'do the thing'");
|
|
42
|
-
});
|
|
43
|
-
test('fork uses --resume <id> --fork-session', () => {
|
|
44
|
-
const cmd = buildAgentCommand({ prompt: 'p', fork: { sessionId: 'abc-123' } }, 'claude');
|
|
45
|
-
assert.equal(cmd, "claude --resume 'abc-123' --fork-session --dangerously-skip-permissions 'p'");
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('buildAgentCommand: pi', () => {
|
|
49
|
-
test('fresh prompt has no skip-permissions flag (pi has no permission popups)', () => {
|
|
50
|
-
const cmd = buildAgentCommand({ prompt: 'do the thing', name: 'worker-1' }, 'pi');
|
|
51
|
-
assert.equal(cmd, "pi -n 'worker-1' 'do the thing'");
|
|
52
|
-
assert.ok(!cmd.includes('--dangerously-skip-permissions'));
|
|
53
|
-
});
|
|
54
|
-
test('fork uses --fork <id>', () => {
|
|
55
|
-
const cmd = buildAgentCommand({ prompt: 'p', fork: { sessionId: 'abc-123' } }, 'pi');
|
|
56
|
-
assert.equal(cmd, "pi --fork 'abc-123' 'p'");
|
|
57
|
-
});
|
|
58
|
-
test('single-quotes in the prompt are escaped safely', () => {
|
|
59
|
-
const cmd = buildAgentCommand({ prompt: "it's fine" }, 'pi');
|
|
60
|
-
assert.equal(cmd, "pi 'it'\\''s fine'");
|
|
61
|
-
});
|
|
62
|
-
});
|
|
63
|
-
describe('buildAgentCommand: subagent persona (systemPrompt/model/tools)', () => {
|
|
64
|
-
test('pi emits --model, --tools, and --append-system-prompt before the prompt', () => {
|
|
65
|
-
const cmd = buildAgentCommand({ prompt: 'task', name: 'scout', systemPrompt: 'You are a scout.', model: 'haiku', tools: ['read', 'grep'] }, 'pi');
|
|
66
|
-
assert.equal(cmd, "pi -n 'scout' --model 'anthropic/haiku' --tools 'read,grep' --append-system-prompt 'You are a scout.' 'task'");
|
|
67
|
-
});
|
|
68
|
-
test('claude emits --model and --append-system-prompt but NOT --tools (different tool model)', () => {
|
|
69
|
-
const cmd = buildAgentCommand({ prompt: 'task', systemPrompt: 'persona', model: 'sonnet', tools: ['read', 'grep'] }, 'claude');
|
|
70
|
-
assert.ok(cmd.includes("--model 'sonnet'"));
|
|
71
|
-
assert.ok(cmd.includes("--append-system-prompt 'persona'"));
|
|
72
|
-
assert.ok(!cmd.includes('--tools'));
|
|
73
|
-
});
|
|
74
|
-
test('omitted persona fields add no flags', () => {
|
|
75
|
-
const cmd = buildAgentCommand({ prompt: 'task' }, 'pi');
|
|
76
|
-
assert.equal(cmd, "pi 'task'");
|
|
77
|
-
});
|
|
78
|
-
});
|
|
79
|
-
describe('buildAgentPrintArgv: subagent persona', () => {
|
|
80
|
-
test('pi threads model/tools/system prompt as discrete args', () => {
|
|
81
|
-
const { args } = buildAgentPrintArgv({ prompt: 'task', systemPrompt: 'persona', model: 'haiku', tools: ['read', 'bash'] }, 'pi');
|
|
82
|
-
assert.deepEqual(args, ['--model', 'anthropic/haiku', '--tools', 'read,bash', '--append-system-prompt', 'persona', '-p', 'task']);
|
|
83
|
-
});
|
|
84
|
-
test('claude threads model/system prompt but not tools', () => {
|
|
85
|
-
const { args } = buildAgentPrintArgv({ prompt: 'task', systemPrompt: 'persona', model: 'sonnet', tools: ['read'] }, 'claude');
|
|
86
|
-
assert.deepEqual(args, ['--model', 'sonnet', '--append-system-prompt', 'persona', '-p', '--dangerously-skip-permissions', 'task']);
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
describe('normalizeModelForKind: pi Claude-alias resolution', () => {
|
|
90
|
-
test('pins bare Claude aliases to the anthropic provider under pi', () => {
|
|
91
|
-
assert.equal(normalizeModelForKind('sonnet', 'pi'), 'anthropic/sonnet');
|
|
92
|
-
assert.equal(normalizeModelForKind('opus', 'pi'), 'anthropic/opus');
|
|
93
|
-
assert.equal(normalizeModelForKind('haiku', 'pi'), 'anthropic/haiku');
|
|
94
|
-
});
|
|
95
|
-
test('preserves a :thinking suffix when pinning the provider', () => {
|
|
96
|
-
assert.equal(normalizeModelForKind('sonnet:high', 'pi'), 'anthropic/sonnet:high');
|
|
97
|
-
});
|
|
98
|
-
test('leaves provider-prefixed and concrete ids untouched under pi', () => {
|
|
99
|
-
assert.equal(normalizeModelForKind('anthropic/sonnet', 'pi'), 'anthropic/sonnet');
|
|
100
|
-
assert.equal(normalizeModelForKind('openai/gpt-4o', 'pi'), 'openai/gpt-4o');
|
|
101
|
-
assert.equal(normalizeModelForKind('claude-sonnet-4-6', 'pi'), 'claude-sonnet-4-6');
|
|
102
|
-
assert.equal(normalizeModelForKind('gpt-4o-mini', 'pi'), 'gpt-4o-mini');
|
|
103
|
-
});
|
|
104
|
-
test('never rewrites for the claude CLI (native alias support)', () => {
|
|
105
|
-
assert.equal(normalizeModelForKind('sonnet', 'claude'), 'sonnet');
|
|
106
|
-
assert.equal(normalizeModelForKind('opus', 'claude'), 'opus');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
describe('buildAgentCommand: defaults to detected kind', () => {
|
|
110
|
-
test('uses pi when PI_CODING_AGENT=true and no explicit kind passed', () => {
|
|
111
|
-
process.env['PI_CODING_AGENT'] = 'true';
|
|
112
|
-
const cmd = buildAgentCommand({ prompt: 'hi' });
|
|
113
|
-
assert.ok(cmd.startsWith('pi '));
|
|
114
|
-
});
|
|
115
|
-
});
|
|
116
|
-
describe('buildAgentPrintArgv (headless print mode)', () => {
|
|
117
|
-
test('pi uses -p and no skip-permissions flag', () => {
|
|
118
|
-
const { cmd, args } = buildAgentPrintArgv({ prompt: 'do it', name: 'w1' }, 'pi');
|
|
119
|
-
assert.equal(cmd, 'pi');
|
|
120
|
-
assert.deepEqual(args, ['-n', 'w1', '-p', 'do it']);
|
|
121
|
-
assert.ok(!args.includes('--dangerously-skip-permissions'));
|
|
122
|
-
});
|
|
123
|
-
test('claude uses -p plus --dangerously-skip-permissions', () => {
|
|
124
|
-
const { cmd, args } = buildAgentPrintArgv({ prompt: 'do it' }, 'claude');
|
|
125
|
-
assert.equal(cmd, 'claude');
|
|
126
|
-
assert.deepEqual(args, ['-p', '--dangerously-skip-permissions', 'do it']);
|
|
127
|
-
});
|
|
128
|
-
test('argv passes the prompt as a discrete arg (no shell quoting)', () => {
|
|
129
|
-
const { args } = buildAgentPrintArgv({ prompt: "it's $weird" }, 'pi');
|
|
130
|
-
assert.equal(args[args.length - 1], "it's $weird");
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
describe('buildAgentPrintCommand (shell string)', () => {
|
|
134
|
-
test('pi prints with -p and shell-quotes the prompt', () => {
|
|
135
|
-
const cmd = buildAgentPrintCommand({ prompt: "it's fine" }, 'pi');
|
|
136
|
-
assert.equal(cmd, "pi '-p' 'it'\\''s fine'");
|
|
137
|
-
});
|
|
138
|
-
});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
// Tests for subagent discovery, resolution, and frontmatter parsing.
|
|
2
|
-
//
|
|
3
|
-
// Run with: node --import tsx/esm --test src/core/__tests__/subagents.test.ts
|
|
4
|
-
import { test, describe, before, after } from 'node:test';
|
|
5
|
-
import assert from 'node:assert/strict';
|
|
6
|
-
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
7
|
-
import { tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { listSubagents, resolveSubagent, subagentId } from '../subagents.js';
|
|
10
|
-
import { resetScopeCache } from '../scope.js';
|
|
11
|
-
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
12
|
-
describe('parseFrontmatterGeneric', () => {
|
|
13
|
-
test('returns raw record including tools/model fields skills ignore', () => {
|
|
14
|
-
const src = '---\nname: scout\ndescription: recon\nmodel: haiku\ntools: read, grep, bash\n---\nBody here.\n';
|
|
15
|
-
const { data, body } = parseFrontmatterGeneric(src);
|
|
16
|
-
assert.ok(data !== null);
|
|
17
|
-
assert.equal(data['name'], 'scout');
|
|
18
|
-
assert.equal(data['description'], 'recon');
|
|
19
|
-
assert.equal(data['model'], 'haiku');
|
|
20
|
-
assert.equal(data['tools'], 'read, grep, bash');
|
|
21
|
-
assert.equal(body, 'Body here.\n');
|
|
22
|
-
});
|
|
23
|
-
test('list-style tools parse to an array', () => {
|
|
24
|
-
const src = '---\nname: x\ndescription: d\ntools:\n - read\n - bash\n---\nb\n';
|
|
25
|
-
const { data } = parseFrontmatterGeneric(src);
|
|
26
|
-
assert.deepEqual(data['tools'], ['read', 'bash']);
|
|
27
|
-
});
|
|
28
|
-
test('no frontmatter yields null data', () => {
|
|
29
|
-
const { data, body } = parseFrontmatterGeneric('just a body');
|
|
30
|
-
assert.equal(data, null);
|
|
31
|
-
assert.equal(body, 'just a body');
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
describe('subagent discovery (project scope)', () => {
|
|
35
|
-
let dir;
|
|
36
|
-
const origCwd = process.cwd();
|
|
37
|
-
before(() => {
|
|
38
|
-
dir = mkdtempSync(join(tmpdir(), 'crtr-subagents-'));
|
|
39
|
-
const agents = join(dir, '.crouter', 'agents');
|
|
40
|
-
mkdirSync(agents, { recursive: true });
|
|
41
|
-
writeFileSync(join(dir, '.crouter', 'config.json'), '{}');
|
|
42
|
-
writeFileSync(join(agents, 'scout.md'), '---\nname: scout\ndescription: Fast recon\nmodel: haiku\ntools: read, grep\n---\nYou are a scout.\n');
|
|
43
|
-
writeFileSync(join(agents, 'reviewer.md'), '---\nname: reviewer\ndescription: Code review\n---\nYou review code.\n');
|
|
44
|
-
// Missing description → skipped from listings.
|
|
45
|
-
writeFileSync(join(agents, 'broken.md'), '---\nname: broken\n---\nno description\n');
|
|
46
|
-
// Name defaults to filename stem when frontmatter omits it.
|
|
47
|
-
writeFileSync(join(agents, 'stemmed.md'), '---\ndescription: named by file\n---\nbody\n');
|
|
48
|
-
process.chdir(dir);
|
|
49
|
-
resetScopeCache();
|
|
50
|
-
});
|
|
51
|
-
after(() => {
|
|
52
|
-
process.chdir(origCwd);
|
|
53
|
-
resetScopeCache();
|
|
54
|
-
rmSync(dir, { recursive: true, force: true });
|
|
55
|
-
});
|
|
56
|
-
test('listSubagents finds valid agents and skips description-less files', () => {
|
|
57
|
-
const ids = listSubagents('project').map(subagentId).sort();
|
|
58
|
-
assert.deepEqual(ids, ['reviewer', 'scout', 'stemmed']);
|
|
59
|
-
});
|
|
60
|
-
test('frontmatter tools comma-string coerces to array; model carried', () => {
|
|
61
|
-
const scout = resolveSubagent('scout', { scope: 'project' });
|
|
62
|
-
assert.deepEqual(scout.frontmatter.tools, ['read', 'grep']);
|
|
63
|
-
assert.equal(scout.frontmatter.model, 'haiku');
|
|
64
|
-
assert.equal(scout.systemPrompt.trim(), 'You are a scout.');
|
|
65
|
-
assert.equal(scout.plugin, '_');
|
|
66
|
-
});
|
|
67
|
-
test('name defaults to filename stem', () => {
|
|
68
|
-
const a = resolveSubagent('stemmed', { scope: 'project' });
|
|
69
|
-
assert.equal(a.name, 'stemmed');
|
|
70
|
-
assert.equal(a.frontmatter.description, 'named by file');
|
|
71
|
-
});
|
|
72
|
-
test('resolveSubagent throws notFound for unknown name', () => {
|
|
73
|
-
assert.throws(() => resolveSubagent('nope', { scope: 'project' }), /subagent not found/);
|
|
74
|
-
});
|
|
75
|
-
});
|