@ludi-uni/ludi-agent-kit 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +55 -0
- package/LICENSE +21 -0
- package/README.md +107 -0
- package/adapters/codex/README.md +24 -0
- package/adapters/codex/skill-metadata/visual-verification/agents/openai.yaml +7 -0
- package/adapters/pi/README.md +88 -0
- package/adapters/pi/browser/agent-browser.mjs +193 -0
- package/adapters/pi/lib/invoke.mjs +55 -0
- package/adapters/pi/lib/list-models.mjs +29 -0
- package/adapters/pi/lib/settings-proposal.mjs +34 -0
- package/adapters/pi/lib/subagent.mjs +175 -0
- package/adapters/pi/loop-guard/index.js +51 -0
- package/adapters/pi/maintenance-policy.json +36 -0
- package/adapters/pi/mcp.template.json +4 -0
- package/adapters/pi/model-catalog.json +97 -0
- package/adapters/pi/models.json +13 -0
- package/adapters/pi/models.local.example.json +14 -0
- package/adapters/pi/orchestrator-ext/command.mjs +14 -0
- package/adapters/pi/orchestrator-ext/index.js +150 -0
- package/adapters/pi/settings.template.json +7 -0
- package/adapters/pi/shell-gate/index.js +70 -0
- package/adapters/pi/sync-pi.ps1 +137 -0
- package/agents/README.md +26 -0
- package/agents/browser.md +64 -0
- package/agents/coder.md +31 -0
- package/agents/orchestrator.md +37 -0
- package/agents/reviewer.md +32 -0
- package/agents/scout.md +35 -0
- package/agents/tester.md +28 -0
- package/agents/visual.md +28 -0
- package/context-pack/SPEC.md +101 -0
- package/context-pack/context-pack.schema.json +79 -0
- package/context-pack/examples/example-fix.md +44 -0
- package/docs/architecture.md +55 -0
- package/docs/migration-from-codex-setting.md +44 -0
- package/docs/model-maintenance.md +401 -0
- package/docs/orchestrator.md +155 -0
- package/docs/phase2-report.md +39 -0
- package/docs/roadmap.md +27 -0
- package/docs/third-party.md +15 -0
- package/lib/agents.mjs +79 -0
- package/lib/context-pack.mjs +215 -0
- package/lib/job.mjs +312 -0
- package/lib/language-policy.mjs +27 -0
- package/lib/maintenance-exec.mjs +377 -0
- package/lib/maintenance-runner.mjs +266 -0
- package/lib/maintenance.mjs +422 -0
- package/lib/normalize.mjs +101 -0
- package/lib/observe/differ.mjs +185 -0
- package/lib/observe/observation.mjs +147 -0
- package/lib/observe/observers.mjs +134 -0
- package/lib/observe/sources.mjs +154 -0
- package/lib/orchestrator/activity.mjs +249 -0
- package/lib/orchestrator/api.mjs +151 -0
- package/lib/orchestrator/contract.mjs +68 -0
- package/lib/orchestrator/escalation.mjs +84 -0
- package/lib/orchestrator/evaluator.mjs +92 -0
- package/lib/orchestrator/failures.mjs +88 -0
- package/lib/orchestrator/health.mjs +53 -0
- package/lib/orchestrator/orchestrator.mjs +483 -0
- package/lib/orchestrator/permissions.mjs +64 -0
- package/lib/orchestrator/planner.mjs +194 -0
- package/lib/orchestrator/policy.mjs +134 -0
- package/lib/orchestrator/router.mjs +45 -0
- package/lib/orchestrator/runner.mjs +278 -0
- package/lib/orchestrator/shell-policy.mjs +52 -0
- package/lib/orchestrator/store.mjs +581 -0
- package/lib/orchestrator/task-store.mjs +79 -0
- package/lib/orchestrator/turn-budget.mjs +63 -0
- package/lib/orchestrator/worktree.mjs +72 -0
- package/lib/pipeline.mjs +279 -0
- package/lib/registry.mjs +63 -0
- package/lib/resolve.mjs +35 -0
- package/lib/routing.mjs +137 -0
- package/lib/telemetry.mjs +222 -0
- package/mcp/README.md +11 -0
- package/mcp/servers.json +13 -0
- package/orchestration/decision-policy.json +66 -0
- package/package.json +56 -0
- package/routing/README.md +24 -0
- package/routing/routing.json +81 -0
- package/routing/routing.schema.json +66 -0
- package/rules/README.md +10 -0
- package/rules/common.md +52 -0
- package/rules/loop-prevention.md +15 -0
- package/rules/repo-local.md +6 -0
- package/scripts/check-environment.ps1 +22 -0
- package/scripts/context-pack.mjs +17 -0
- package/scripts/e2e-investigate-repro.mjs +66 -0
- package/scripts/model-maintenance-job.mjs +59 -0
- package/scripts/observe-models.mjs +97 -0
- package/scripts/orchestrate.mjs +137 -0
- package/scripts/reevaluate-models.mjs +95 -0
- package/scripts/report-model-maintenance.mjs +70 -0
- package/scripts/resolve-capabilities.mjs +39 -0
- package/scripts/run-pipeline.mjs +56 -0
- package/scripts/sync-agents-md.ps1 +10 -0
- package/scripts/validate.mjs +71 -0
- package/skills/README.md +14 -0
- package/skills/pi-workflow/SKILL.md +26 -0
- package/skills/pi-workflow/references/code-investigation-and-fix.md +16 -0
- package/skills/pi-workflow/references/research.md +14 -0
- package/skills/pi-workflow/references/review.md +11 -0
- package/skills/pi-workflow/references/visual-work.md +14 -0
- package/skills/project-management/SKILL.md +106 -0
- package/skills/project-management/references/operations.md +52 -0
- package/skills/visual-verification/SKILL.md +88 -0
- package/skills/visual-verification/scripts/analyze-speech.ps1 +346 -0
- package/skills/visual-verification/scripts/backends/whisperx_backend.py +234 -0
- package/skills/visual-verification/scripts/common.ps1 +387 -0
- package/skills/visual-verification/scripts/contact-sheet.ps1 +121 -0
- package/skills/visual-verification/scripts/desktop-discover.ps1 +45 -0
- package/skills/visual-verification/scripts/desktop-inspect.ps1 +67 -0
- package/skills/visual-verification/scripts/desktop-record.ps1 +97 -0
- package/skills/visual-verification/scripts/desktop-screenshot.ps1 +65 -0
- package/skills/visual-verification/scripts/evaluate-sync.ps1 +249 -0
- package/skills/visual-verification/scripts/extract-frames.ps1 +79 -0
- package/skills/visual-verification/scripts/inspect-media.ps1 +138 -0
- package/skills/visual-verification/scripts/record-av.ps1 +102 -0
- package/skills/visual-verification/scripts/record.ps1 +72 -0
- package/skills/visual-verification/scripts/screenshot.ps1 +44 -0
- package/skills/visual-verification/scripts/waveform.ps1 +450 -0
- package/skills/visual-verification/scripts/winapp-common.ps1 +465 -0
- package/tests/activity.test.mjs +252 -0
- package/tests/attempt-budget.test.mjs +102 -0
- package/tests/browser.test.mjs +121 -0
- package/tests/context-pack.test.mjs +98 -0
- package/tests/dirty-gate.test.mjs +211 -0
- package/tests/e2e-browser.mjs +66 -0
- package/tests/e2e-real-orchestrator-resume.mjs +101 -0
- package/tests/e2e-real-orchestrator.mjs +41 -0
- package/tests/e2e-real-pi.mjs +27 -0
- package/tests/e2e-real-tool-orchestrator.mjs +66 -0
- package/tests/fixtures/browser-page/index.html +20 -0
- package/tests/fixtures/maintenance/availability.txt +5 -0
- package/tests/fixtures/maintenance/catalog.json +74 -0
- package/tests/fixtures/maintenance/events.json +13 -0
- package/tests/fixtures/math-repo/README.md +3 -0
- package/tests/fixtures/math-repo/package.json +7 -0
- package/tests/fixtures/math-repo/src/math.js +11 -0
- package/tests/fixtures/math-repo/test/math.test.js +7 -0
- package/tests/fixtures/observe/announcements.json +8 -0
- package/tests/fixtures/orch-concurrent-child.mjs +44 -0
- package/tests/fixtures/orch-persist-child.mjs +61 -0
- package/tests/job.test.mjs +230 -0
- package/tests/kit.test.mjs +79 -0
- package/tests/language-policy.test.mjs +93 -0
- package/tests/loop-guard.test.mjs +60 -0
- package/tests/maintenance-exec.test.mjs +218 -0
- package/tests/maintenance-runner.test.mjs +222 -0
- package/tests/maintenance.test.mjs +195 -0
- package/tests/observe.test.mjs +283 -0
- package/tests/observer-registry.test.mjs +157 -0
- package/tests/orchestrator-cleanup.test.mjs +358 -0
- package/tests/orchestrator-command.test.mjs +14 -0
- package/tests/orchestrator-persist.test.mjs +375 -0
- package/tests/orchestrator-tools.test.mjs +215 -0
- package/tests/orchestrator.test.mjs +396 -0
- package/tests/package.test.mjs +37 -0
- package/tests/pipeline.test.mjs +239 -0
- package/tests/planner-classification.test.mjs +81 -0
- package/tests/planner-split.test.mjs +67 -0
- package/tests/qoder-observer.test.mjs +266 -0
- package/tests/reassign-progression.test.mjs +104 -0
- package/tests/retry-escalation.test.mjs +120 -0
- package/tests/routing.test.mjs +110 -0
- package/tests/sqlite-concurrency.test.mjs +178 -0
- package/tests/task-global-e2e.test.mjs +63 -0
- package/tests/task-global-failed.test.mjs +134 -0
- package/tests/telemetry.test.mjs +173 -0
- package/tests/test-sync-pi.ps1 +56 -0
- package/tests/turn-budget.test.mjs +106 -0
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
// Safe orchestrator run-history cleanup: preview/prune/delete/clear.
|
|
2
|
+
// Safety contract:
|
|
3
|
+
// - default cleanup only deletes terminal runs (completed/failed/cancelled)
|
|
4
|
+
// without pending decisions; active/resumable runs require explicit force
|
|
5
|
+
// (--clear --force --include-active or --delete ID --force).
|
|
6
|
+
// - every delete runs inside one BEGIN IMMEDIATE transaction; an injected failure
|
|
7
|
+
// mid-batch rolls the whole batch back.
|
|
8
|
+
// - decision_memory, protocol_stats and GLOBAL backend health (run_id = '') are
|
|
9
|
+
// never touched; run-local health rows go with their run.
|
|
10
|
+
// - all tests use temp dirs; the real .orchestration/state.db is never written.
|
|
11
|
+
import test from 'node:test';
|
|
12
|
+
import assert from 'node:assert/strict';
|
|
13
|
+
import { spawnSync } from 'node:child_process';
|
|
14
|
+
import { mkdtempSync } from 'node:fs';
|
|
15
|
+
import { tmpdir } from 'node:os';
|
|
16
|
+
import { join, resolve, dirname } from 'node:path';
|
|
17
|
+
import { fileURLToPath } from 'node:url';
|
|
18
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
19
|
+
import { openStore } from '../lib/orchestrator/store.mjs';
|
|
20
|
+
import { newTask } from '../lib/orchestrator/task-store.mjs';
|
|
21
|
+
import { DEFAULT_POLICY } from '../lib/orchestrator/policy.mjs';
|
|
22
|
+
import { parseOlderThan, formatCleanup, resumeOrchestration } from '../lib/orchestrator/api.mjs';
|
|
23
|
+
|
|
24
|
+
const kit = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
25
|
+
const cli = join(kit, 'scripts/orchestrate.mjs');
|
|
26
|
+
const dbPath = () => join(mkdtempSync(join(tmpdir(), 'ludi-clean-')), 'state.db');
|
|
27
|
+
const runCli = args => spawnSync(process.execPath, [cli, ...args], { encoding: 'utf8' });
|
|
28
|
+
|
|
29
|
+
const seedTask = (session, runId, status = 'completed') => {
|
|
30
|
+
const store = session.openTaskStore(runId);
|
|
31
|
+
store.add(newTask({ id: 't1', title: 't', goal: 'g', capability: 'cheap-code', assignedAgent: 'scout', acceptance: ['done'] }));
|
|
32
|
+
store.update('t1', { status });
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const seedTraceAndHealth = (session, runId) => {
|
|
36
|
+
session.appendTrace(runId, { at: '2026-01-01T00:00:00.000Z', round: 1, type: 'result' });
|
|
37
|
+
session.recordHealth({ provider: 'ps', model: 'ms', state: 'rate_limited', runId, ttlMs: 60_000 });
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const counts = session => {
|
|
41
|
+
const p = session.previewRuns({ includeActive: true });
|
|
42
|
+
return {
|
|
43
|
+
runs: p.runs.length,
|
|
44
|
+
tasks: p.runs.reduce((n, r) => n + r.counts.tasks, 0),
|
|
45
|
+
decisions: p.runs.reduce((n, r) => n + r.counts.decisions, 0),
|
|
46
|
+
trace: p.runs.reduce((n, r) => n + r.counts.trace, 0),
|
|
47
|
+
health: p.runs.reduce((n, r) => n + r.counts.health, 0),
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
test('A: previewRuns classifies terminal vs active and pending decisions; nothing is written', () => {
|
|
52
|
+
const session = openStore(dbPath());
|
|
53
|
+
const done = session.createRun({ request: 'done', policy: DEFAULT_POLICY });
|
|
54
|
+
session.updateRun(done, { status: 'completed' });
|
|
55
|
+
const running = session.createRun({ request: 'live', policy: DEFAULT_POLICY });
|
|
56
|
+
const waiting = session.createRun({ request: 'wait', policy: DEFAULT_POLICY });
|
|
57
|
+
session.updateRun(waiting, { status: 'waiting_for_user' });
|
|
58
|
+
session.insertDecision({ runId: waiting, taskId: 't1', question: 'pick one', options: [{ id: 'a' }] });
|
|
59
|
+
// Terminal status but a pending decision -> still resumable, not deletable.
|
|
60
|
+
const zombie = session.createRun({ request: 'zombie', policy: DEFAULT_POLICY });
|
|
61
|
+
session.updateRun(zombie, { status: 'failed' });
|
|
62
|
+
session.insertDecision({ runId: zombie, taskId: 't1', question: 'pick one', options: [{ id: 'a' }] });
|
|
63
|
+
|
|
64
|
+
const before = session.listRuns().length;
|
|
65
|
+
const preview = session.previewRuns();
|
|
66
|
+
assert.equal(session.listRuns().length, before, 'preview wrote nothing');
|
|
67
|
+
assert.deepEqual(preview.deletable, [done]);
|
|
68
|
+
assert.match(preview.runs.find(r => r.id === zombie).reason, /pending decision/);
|
|
69
|
+
assert.match(preview.runs.find(r => r.id === running).reason, /active\/resumable/);
|
|
70
|
+
assert.match(preview.runs.find(r => r.id === waiting).reason, /active\/resumable/);
|
|
71
|
+
const all = session.previewRuns({ includeActive: true });
|
|
72
|
+
assert.deepEqual(new Set(all.deletable), new Set([done, running, waiting, zombie]));
|
|
73
|
+
session.close();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('B: pruneRuns deletes terminal runs and cascades tasks/decisions/trace/run-local health', () => {
|
|
77
|
+
const session = openStore(dbPath());
|
|
78
|
+
const runId = session.createRun({ request: 'old', policy: DEFAULT_POLICY });
|
|
79
|
+
seedTask(session, runId);
|
|
80
|
+
session.updateRun(runId, { status: 'completed' });
|
|
81
|
+
session.insertDecision({ runId, taskId: 't1', question: 'q', options: [] });
|
|
82
|
+
session.answerDecision({ runId, decisionId: session.listDecisions(runId)[0].id, answer: 'yes' });
|
|
83
|
+
seedTraceAndHealth(session, runId);
|
|
84
|
+
const result = session.pruneRuns();
|
|
85
|
+
assert.equal(result.executed, true);
|
|
86
|
+
assert.deepEqual(result.deletable, [runId]);
|
|
87
|
+
assert.equal(result.totals.tasks, 1);
|
|
88
|
+
assert.equal(result.totals.decisions, 1);
|
|
89
|
+
assert.equal(result.totals.trace, 1);
|
|
90
|
+
assert.equal(result.totals.health, 1);
|
|
91
|
+
assert.equal(session.getRun(runId), null);
|
|
92
|
+
assert.equal(session.loadTasks(runId).length, 0);
|
|
93
|
+
assert.equal(session.listDecisions(runId).length, 0);
|
|
94
|
+
assert.equal(session.loadTrace(runId).length, 0);
|
|
95
|
+
assert.equal(session.activeHealth({ provider: 'ps', model: 'ms', runId }), null);
|
|
96
|
+
session.close();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('C: olderThan cutoff keeps recent terminal runs', () => {
|
|
100
|
+
const session = openStore(dbPath());
|
|
101
|
+
const old = session.createRun({ request: 'old', policy: DEFAULT_POLICY });
|
|
102
|
+
session.updateRun(old, { status: 'completed' });
|
|
103
|
+
const fresh = session.createRun({ request: 'fresh', policy: DEFAULT_POLICY });
|
|
104
|
+
session.updateRun(fresh, { status: 'completed' });
|
|
105
|
+
const cutoff = new Date(Date.now() - 1000).toISOString(); // everything older than 1s ago
|
|
106
|
+
const none = session.previewRuns({ olderThan: cutoff });
|
|
107
|
+
assert.equal(none.deletable.length, 0, 'both runs were just updated');
|
|
108
|
+
const future = new Date(Date.now() + 60_000).toISOString();
|
|
109
|
+
const all = session.pruneRuns({ olderThan: future });
|
|
110
|
+
assert.equal(all.totals.runs, 2);
|
|
111
|
+
assert.equal(session.listRuns().length, 0);
|
|
112
|
+
session.close();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('D: decision_memory, protocol_stats and global health survive any cleanup', () => {
|
|
116
|
+
const session = openStore(dbPath());
|
|
117
|
+
const runId = session.createRun({ request: 'r', policy: DEFAULT_POLICY });
|
|
118
|
+
session.updateRun(runId, { status: 'completed' });
|
|
119
|
+
session.saveMemory({ scope: 'repository', scopeKey: 'default', key: 'lib', decision: { optionId: 'a' }, rationale: 'r' });
|
|
120
|
+
session.recordProtocol({ provider: 'ps', model: 'ms', kind: 'malformed' });
|
|
121
|
+
session.recordHealth({ provider: 'pc', model: 'mc', state: 'usage_exhausted', runId, ttlMs: 60_000 }); // stored with run_id=''
|
|
122
|
+
const cleared = session.clearRuns({ force: true, includeActive: true });
|
|
123
|
+
assert.equal(cleared.totals.runs, 1);
|
|
124
|
+
assert.equal(session.listRuns().length, 0);
|
|
125
|
+
assert.equal(session.listMemory().length, 1);
|
|
126
|
+
assert.equal(session.protocolStats().length, 1);
|
|
127
|
+
assert.equal(session.protocolStats()[0].malformed, 1);
|
|
128
|
+
const global = session.activeHealth({ provider: 'pc', model: 'mc' });
|
|
129
|
+
assert.ok(global, 'global usage_exhausted health is preserved');
|
|
130
|
+
assert.equal(global.runId, undefined);
|
|
131
|
+
session.close();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('E: injected failure inside pruneRuns rolls the whole batch back', () => {
|
|
135
|
+
const path = dbPath();
|
|
136
|
+
const session = openStore(path);
|
|
137
|
+
const a = session.createRun({ request: 'a', policy: DEFAULT_POLICY });
|
|
138
|
+
session.updateRun(a, { status: 'completed' });
|
|
139
|
+
const b = session.createRun({ request: 'b', policy: DEFAULT_POLICY });
|
|
140
|
+
session.updateRun(b, { status: 'failed' });
|
|
141
|
+
session.insertDecision({ runId: b, taskId: 't1', question: 'q', options: [] });
|
|
142
|
+
const dec = session.listDecisions(b)[0];
|
|
143
|
+
session.answerDecision({ runId: b, decisionId: dec.id, answer: 'ok' });
|
|
144
|
+
// Corrupt the answered decision so the mid-transaction DELETE fails with a
|
|
145
|
+
// constraint error — proves rollback, not partial cleanup.
|
|
146
|
+
const raw = new DatabaseSync(path);
|
|
147
|
+
raw.exec(`CREATE TRIGGER fail_on_decision_delete BEFORE DELETE ON decisions
|
|
148
|
+
WHEN OLD.run_id = '${b}' BEGIN SELECT RAISE(ABORT, 'injected constraint'); END;`);
|
|
149
|
+
raw.close();
|
|
150
|
+
assert.throws(() => session.pruneRuns(), /injected|abort/i);
|
|
151
|
+
assert.ok(session.getRun(a), 'first run still present after rollback');
|
|
152
|
+
assert.ok(session.getRun(b), 'second run still present after rollback');
|
|
153
|
+
assert.equal(session.listDecisions(b).length, 1);
|
|
154
|
+
session.close();
|
|
155
|
+
const cleanup = new DatabaseSync(path);
|
|
156
|
+
cleanup.exec('DROP TRIGGER fail_on_decision_delete');
|
|
157
|
+
cleanup.close();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('F: deleteRun refuses active and pending-decision runs; --force deletes terminal', () => {
|
|
161
|
+
const session = openStore(dbPath());
|
|
162
|
+
const running = session.createRun({ request: 'live', policy: DEFAULT_POLICY });
|
|
163
|
+
assert.throws(() => session.deleteRun(running), /active\/resumable; use --force/);
|
|
164
|
+
const waiting = session.createRun({ request: 'w', policy: DEFAULT_POLICY });
|
|
165
|
+
session.updateRun(waiting, { status: 'waiting_for_user' });
|
|
166
|
+
session.insertDecision({ runId: waiting, taskId: 't1', question: 'q', options: [] });
|
|
167
|
+
assert.throws(() => session.deleteRun(waiting), /active\/resumable; use --force/);
|
|
168
|
+
assert.equal(session.deleteRun(waiting, { force: true }).id, waiting);
|
|
169
|
+
const done = session.createRun({ request: 'd', policy: DEFAULT_POLICY });
|
|
170
|
+
session.updateRun(done, { status: 'cancelled' });
|
|
171
|
+
const out = session.deleteRun(done);
|
|
172
|
+
assert.equal(out.id, done);
|
|
173
|
+
assert.equal(session.getRun(done), null);
|
|
174
|
+
assert.throws(() => session.deleteRun('run-nope'), /run not found/);
|
|
175
|
+
session.close();
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('G: clearRuns previews by default; --force deletes terminal only; --include-active adds active runs', () => {
|
|
179
|
+
const session = openStore(dbPath());
|
|
180
|
+
const done = session.createRun({ request: 'done', policy: DEFAULT_POLICY });
|
|
181
|
+
session.updateRun(done, { status: 'completed' });
|
|
182
|
+
const live = session.createRun({ request: 'live', policy: DEFAULT_POLICY });
|
|
183
|
+
const waiting = session.createRun({ request: 'wait', policy: DEFAULT_POLICY });
|
|
184
|
+
session.updateRun(waiting, { status: 'waiting_for_user' });
|
|
185
|
+
session.insertDecision({ runId: waiting, taskId: 't1', question: 'q', options: [] });
|
|
186
|
+
|
|
187
|
+
const preview = session.clearRuns();
|
|
188
|
+
assert.equal(preview.executed, false);
|
|
189
|
+
assert.deepEqual(preview.deletable, [done]);
|
|
190
|
+
assert.equal(session.listRuns().length, 3, 'preview wrote nothing');
|
|
191
|
+
const totals = counts(session);
|
|
192
|
+
assert.equal(totals.runs, 3);
|
|
193
|
+
|
|
194
|
+
const forced = session.clearRuns({ force: true });
|
|
195
|
+
assert.equal(forced.executed, true);
|
|
196
|
+
assert.deepEqual(forced.deletable, [done]);
|
|
197
|
+
assert.ok(session.getRun(live), 'active run survived --force without --include-active');
|
|
198
|
+
assert.ok(session.getRun(waiting), 'pending-decision run survived');
|
|
199
|
+
|
|
200
|
+
const all = session.clearRuns({ force: true, includeActive: true });
|
|
201
|
+
assert.deepEqual(new Set(all.deletable), new Set([live, waiting]), 'explicit include-active removes both active and waiting');
|
|
202
|
+
assert.equal(session.listRuns().length, 0);
|
|
203
|
+
session.close();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('H: CLI --prune honours --older-than and prints Japanese output', () => {
|
|
207
|
+
const path = dbPath();
|
|
208
|
+
const session = openStore(path);
|
|
209
|
+
const r = session.createRun({ request: 'old run', policy: DEFAULT_POLICY });
|
|
210
|
+
session.updateRun(r, { status: 'completed' });
|
|
211
|
+
session.close();
|
|
212
|
+
|
|
213
|
+
const bad = runCli(['--prune', '--older-than', 'soon', '--store', path]);
|
|
214
|
+
assert.equal(bad.status, 2);
|
|
215
|
+
assert.match(bad.stderr, /invalid --older-than/);
|
|
216
|
+
const misplaced = runCli(['--older-than', '7d', '--list', '--store', path]);
|
|
217
|
+
assert.equal(misplaced.status, 2);
|
|
218
|
+
assert.match(misplaced.stderr, /--older-than requires --prune/);
|
|
219
|
+
|
|
220
|
+
const keep = runCli(['--prune', '--older-than', '7d', '--store', path]);
|
|
221
|
+
assert.equal(keep.status, 0, keep.stderr);
|
|
222
|
+
assert.match(keep.stdout, /削除しました: 0 ラン/);
|
|
223
|
+
const gone = runCli(['--prune', '--store', path]);
|
|
224
|
+
assert.equal(gone.status, 0, gone.stderr);
|
|
225
|
+
assert.match(gone.stdout, /削除しました: 0 ラン/);
|
|
226
|
+
const db = new DatabaseSync(path);
|
|
227
|
+
db.prepare('UPDATE runs SET updated_at = ? WHERE id = ?').run('2020-01-01T00:00:00.000Z', r);
|
|
228
|
+
db.close();
|
|
229
|
+
const preview = runCli(['--prune', '--dry-run', '--store', path]);
|
|
230
|
+
assert.equal(preview.status, 0, preview.stderr);
|
|
231
|
+
assert.match(preview.stdout, /削除対象(プレビュー): 1 ラン/);
|
|
232
|
+
const aged = runCli(['--prune', '--store', path]);
|
|
233
|
+
assert.equal(aged.status, 0, aged.stderr);
|
|
234
|
+
assert.match(aged.stdout, /削除しました: 1 ラン/);
|
|
235
|
+
const check = openStore(path);
|
|
236
|
+
assert.equal(check.listRuns().length, 0);
|
|
237
|
+
check.close();
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test('I: CLI --delete removes terminal runs and requires --force for active runs', () => {
|
|
241
|
+
const path = dbPath();
|
|
242
|
+
const session = openStore(path);
|
|
243
|
+
const done = session.createRun({ request: 'finished', policy: DEFAULT_POLICY });
|
|
244
|
+
session.updateRun(done, { status: 'completed' });
|
|
245
|
+
const live = session.createRun({ request: 'live', policy: DEFAULT_POLICY });
|
|
246
|
+
session.close();
|
|
247
|
+
|
|
248
|
+
const blocked = runCli(['--delete', live, '--store', path]);
|
|
249
|
+
assert.notEqual(blocked.status, 0);
|
|
250
|
+
assert.match(blocked.stderr, /active\/resumable; use --force/);
|
|
251
|
+
const del = runCli(['--delete', done, '--store', path]);
|
|
252
|
+
assert.equal(del.status, 0, del.stderr);
|
|
253
|
+
assert.match(del.stdout, new RegExp(`削除しました: ${done}`));
|
|
254
|
+
const missing = runCli(['--delete', done, '--store', path]);
|
|
255
|
+
assert.equal(missing.status, 1);
|
|
256
|
+
assert.match(missing.stderr, /run not found/);
|
|
257
|
+
const forced = runCli(['--delete', live, '--force', '--store', path]);
|
|
258
|
+
assert.equal(forced.status, 0, forced.stderr);
|
|
259
|
+
assert.match(forced.stdout, new RegExp(`削除しました: ${live}`));
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('J: CLI --clear previews, --force clears terminal, --include-active is explicit', () => {
|
|
263
|
+
const path = dbPath();
|
|
264
|
+
const session = openStore(path);
|
|
265
|
+
const done = session.createRun({ request: 'done', policy: DEFAULT_POLICY });
|
|
266
|
+
session.updateRun(done, { status: 'completed' });
|
|
267
|
+
const live = session.createRun({ request: 'live', policy: DEFAULT_POLICY });
|
|
268
|
+
session.close();
|
|
269
|
+
|
|
270
|
+
const preview = runCli(['--clear', '--store', path]);
|
|
271
|
+
assert.equal(preview.status, 0, preview.stderr);
|
|
272
|
+
assert.match(preview.stdout, /削除対象(プレビュー): 1 ラン/);
|
|
273
|
+
assert.match(preview.stdout, /--force/);
|
|
274
|
+
const forced = runCli(['--clear', '--force', '--store', path]);
|
|
275
|
+
assert.equal(forced.status, 0, forced.stderr);
|
|
276
|
+
assert.match(forced.stdout, /削除しました: 1 ラン/);
|
|
277
|
+
const check1 = openStore(path);
|
|
278
|
+
assert.equal(check1.listRuns().length, 1, 'active run survived');
|
|
279
|
+
check1.close();
|
|
280
|
+
const active = runCli(['--clear', '--force', '--include-active', '--store', path]);
|
|
281
|
+
assert.equal(active.status, 0, active.stderr);
|
|
282
|
+
assert.match(active.stdout, /削除しました: 1 ラン/);
|
|
283
|
+
const check2 = openStore(path);
|
|
284
|
+
assert.equal(check2.listRuns().length, 0);
|
|
285
|
+
check2.close();
|
|
286
|
+
const bad = runCli(['--include-active', '--prune', '--store', path]);
|
|
287
|
+
assert.equal(bad.status, 2);
|
|
288
|
+
assert.match(bad.stderr, /--include-active requires --clear/);
|
|
289
|
+
const bad2 = runCli(['--force', '--list', '--store', path]);
|
|
290
|
+
assert.equal(bad2.status, 2);
|
|
291
|
+
assert.match(bad2.stderr, /--force requires --delete or --clear/);
|
|
292
|
+
const bad3 = runCli(['--prune', '--clear', '--store', path]);
|
|
293
|
+
assert.equal(bad3.status, 2);
|
|
294
|
+
assert.match(bad3.stderr, /exclusive/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('K: parseOlderThan validates durations', () => {
|
|
298
|
+
assert.equal(parseOlderThan('7d'), 7 * 86_400_000);
|
|
299
|
+
assert.equal(parseOlderThan('12h'), 12 * 3_600_000);
|
|
300
|
+
assert.equal(parseOlderThan('30m'), 30 * 60_000);
|
|
301
|
+
assert.equal(parseOlderThan('2w'), 2 * 604_800_000);
|
|
302
|
+
for (const bad of ['', 'abc', '7', 'd7', '-3d', '1.5d', '7y']) {
|
|
303
|
+
assert.throws(() => parseOlderThan(bad), /invalid --older-than/, bad);
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test('L: formatCleanup renders concise Japanese for preview and executed results', () => {
|
|
308
|
+
const session = openStore(dbPath());
|
|
309
|
+
const r = session.createRun({ request: 'some request', policy: DEFAULT_POLICY });
|
|
310
|
+
session.updateRun(r, { status: 'completed' });
|
|
311
|
+
const preview = session.clearRuns();
|
|
312
|
+
const text = formatCleanup(preview);
|
|
313
|
+
assert.match(text, /削除対象(プレビュー): 1 ラン/);
|
|
314
|
+
assert.match(text, /--force/);
|
|
315
|
+
const done = formatCleanup(session.clearRuns({ force: true }));
|
|
316
|
+
assert.match(done, /削除しました: 1 ラン/);
|
|
317
|
+
assert.equal(formatCleanup(session.clearRuns()), '削除対象のランはありません');
|
|
318
|
+
session.close();
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test('M: cleanup never touches the real .orchestration/state.db in tests', () => {
|
|
322
|
+
// Every test above uses mkdtempSync paths; this asserts the convention and that
|
|
323
|
+
// the CLI never writes to the default store when --store is given a temp path.
|
|
324
|
+
const realDefault = join(kit, '.orchestration', 'state.db');
|
|
325
|
+
const path = dbPath();
|
|
326
|
+
assert.notEqual(resolve(path), resolve(realDefault));
|
|
327
|
+
const out = runCli(['--clear', '--store', path]);
|
|
328
|
+
assert.equal(out.status, 0, out.stderr);
|
|
329
|
+
assert.match(out.stdout, /削除対象のランはありません/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test('N: a terminal run with an in-flight task is protected until the task settles', () => {
|
|
333
|
+
const session = openStore(dbPath());
|
|
334
|
+
const runId = session.createRun({ request: 'crashed', policy: DEFAULT_POLICY });
|
|
335
|
+
seedTask(session, runId, 'running');
|
|
336
|
+
session.updateRun(runId, { status: 'failed' });
|
|
337
|
+
const preview = session.previewRuns();
|
|
338
|
+
assert.deepEqual(preview.deletable, []);
|
|
339
|
+
assert.match(preview.runs.find(r => r.id === runId).reason, /running task/);
|
|
340
|
+
assert.throws(() => session.deleteRun(runId), /active\/resumable; use --force/);
|
|
341
|
+
session.openTaskStore(runId).update('t1', { status: 'failed' });
|
|
342
|
+
const out = session.deleteRun(runId);
|
|
343
|
+
assert.equal(out.deleted.tasks, 1);
|
|
344
|
+
assert.equal(session.loadTasks(runId).length, 0);
|
|
345
|
+
session.close();
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test('O: list/show/resume of a deleted run do not revive it', async () => {
|
|
349
|
+
const session = openStore(dbPath());
|
|
350
|
+
const id = session.createRun({ request: 'finished', policy: DEFAULT_POLICY });
|
|
351
|
+
session.updateRun(id, { status: 'completed' });
|
|
352
|
+
session.deleteRun(id);
|
|
353
|
+
assert.equal(session.getRun(id), null);
|
|
354
|
+
assert.ok(!session.listRuns().some(r => r.id === id));
|
|
355
|
+
await assert.rejects(() => resumeOrchestration({ session, agents: [], routing: {}, registry: {}, policy: DEFAULT_POLICY },
|
|
356
|
+
{ runId: id, runner: { run() { throw new Error('must not invoke'); } } }), /run not found/);
|
|
357
|
+
session.close();
|
|
358
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { parseOrchestrateCommand } from '../adapters/pi/orchestrator-ext/command.mjs';
|
|
4
|
+
|
|
5
|
+
test('read-only slash commands route without starting a new run', () => {
|
|
6
|
+
for (const action of ['status', 'children', 'result']) {
|
|
7
|
+
assert.deepEqual(parseOrchestrateCommand(`${action} run-123`), { action, params: { runId: 'run-123' } });
|
|
8
|
+
}
|
|
9
|
+
assert.deepEqual(parseOrchestrateCommand(''), { action: 'list', params: { runId: undefined } });
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('a natural-language request remains a new start', () => {
|
|
13
|
+
assert.deepEqual(parseOrchestrateCommand('Fix the test'), { action: 'start', params: { request: 'Fix the test' } });
|
|
14
|
+
});
|