@dotdrelle/wiki-manager 0.15.43 → 0.15.49
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/README.md +92 -24
- package/mcp.endpoints.example.json +1 -1
- package/package.json +2 -2
- package/src/agent/graph.js +288 -22
- package/src/agent/graph.test.js +551 -1
- package/src/agent/skillRecursion.test.js +98 -0
- package/src/cli/wiki-manager.js +209 -7
- package/src/cli/wiki-manager.test.js +89 -0
- package/src/commands/slash.js +28 -10
- package/src/contracts/schemas.js +1 -1
- package/src/core/agentEvents.js +50 -0
- package/src/core/agentEvents.test.js +52 -0
- package/src/core/buildInfo.json +2 -2
- package/src/core/env.js +20 -1
- package/src/core/env.test.js +34 -0
- package/src/core/mcp.js +1 -1
- package/src/core/runtimeLog.js +15 -0
- package/src/core/runtimeLog.test.js +15 -1
- package/src/core/skillChainView.js +84 -0
- package/src/core/skillChainView.test.js +50 -0
- package/src/core/skillCompiler.js +135 -0
- package/src/core/skillCompiler.test.js +91 -0
- package/src/core/skillInvocation.js +79 -0
- package/src/core/skillInvocation.test.js +73 -0
- package/src/core/skills.js +81 -19
- package/src/runtime/approvals.js +13 -1
- package/src/runtime/approvals.test.js +44 -0
- package/src/runtime/client.js +45 -4
- package/src/runtime/controlCancellation.js +33 -0
- package/src/runtime/controlCancellation.test.js +49 -0
- package/src/runtime/controlDrain.js +50 -0
- package/src/runtime/controlDrain.test.js +38 -0
- package/src/runtime/server.js +333 -15
- package/src/runtime/server.test.js +392 -0
- package/src/runtime/skillChain.e2e.test.js +394 -0
- package/src/runtime/skillRun.js +104 -0
- package/src/runtime/skillRun.test.js +84 -0
- package/src/runtime/store.js +69 -0
- package/src/runtime/store.test.js +11 -0
- package/src/shell/RightPane.tsx +3 -2
- package/src/shell/repl.js +40 -5
- package/src/shell/repl.test.js +41 -0
- package/src/shell/useSession.ts +43 -9
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import test from 'node:test';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
3
6
|
import { createAgentEvent, dispatchAgentEvent } from '../core/agentEvents.js';
|
|
4
7
|
import { createInteractiveSession, ensureInteractiveAssistantMessage } from '../cli/wiki-manager.js';
|
|
8
|
+
import { postRuntimeRun } from './client.js';
|
|
5
9
|
import { approvalRequestFromStatus, runtimeState, startRuntimeServer as startRuntimeServerImpl } from './server.js';
|
|
6
10
|
|
|
7
11
|
// Most server tests exercise endpoint behavior rather than authentication. Keep
|
|
@@ -11,6 +15,45 @@ function startRuntimeServer(options) {
|
|
|
11
15
|
return startRuntimeServerImpl({ token: '', ...options });
|
|
12
16
|
}
|
|
13
17
|
|
|
18
|
+
test('runtime run client surfaces the server error instead of only the HTTP status', async () => {
|
|
19
|
+
const originalFetch = globalThis.fetch;
|
|
20
|
+
globalThis.fetch = async () => new Response(JSON.stringify({
|
|
21
|
+
error: 'Workspace services are unavailable. Start them with /start all, then retry.',
|
|
22
|
+
code: 'services_unavailable',
|
|
23
|
+
}), {
|
|
24
|
+
status: 400,
|
|
25
|
+
headers: { 'Content-Type': 'application/json' },
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
await assert.rejects(
|
|
30
|
+
postRuntimeRun('build the wiki', { workspace: 'demo' }),
|
|
31
|
+
(error) => {
|
|
32
|
+
assert.equal(error.message, 'Runtime run failed: HTTP 400 — Workspace services are unavailable. Start them with /start all, then retry.');
|
|
33
|
+
assert.equal(error.status, 400);
|
|
34
|
+
assert.equal(error.code, 'services_unavailable');
|
|
35
|
+
return true;
|
|
36
|
+
},
|
|
37
|
+
);
|
|
38
|
+
} finally {
|
|
39
|
+
globalThis.fetch = originalFetch;
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('runtime run client keeps the HTTP fallback without a JSON error body', async () => {
|
|
44
|
+
const originalFetch = globalThis.fetch;
|
|
45
|
+
globalThis.fetch = async () => new Response('Bad request', { status: 400 });
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
await assert.rejects(
|
|
49
|
+
postRuntimeRun('build the wiki', { workspace: 'demo' }),
|
|
50
|
+
/Runtime run failed: HTTP 400/,
|
|
51
|
+
);
|
|
52
|
+
} finally {
|
|
53
|
+
globalThis.fetch = originalFetch;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
14
57
|
test('approval fallback derives classes from waiting tasks when the approval queue is missing', () => {
|
|
15
58
|
const request = approvalRequestFromStatus({
|
|
16
59
|
workspace: 'acme',
|
|
@@ -685,6 +728,57 @@ test('runtime server kill can target a specific run id', async (t) => {
|
|
|
685
728
|
}
|
|
686
729
|
});
|
|
687
730
|
|
|
731
|
+
test('runtime server kill purge without workspace is refused', async (t) => {
|
|
732
|
+
let clearedArgs = null;
|
|
733
|
+
let handle;
|
|
734
|
+
try {
|
|
735
|
+
handle = await startRuntimeServer({
|
|
736
|
+
host: '127.0.0.1',
|
|
737
|
+
port: 0,
|
|
738
|
+
store: {
|
|
739
|
+
dbPath: ':memory:',
|
|
740
|
+
getState: () => ({ status: 'idle' }),
|
|
741
|
+
listEvents: () => [],
|
|
742
|
+
interruptRuns: () => 0,
|
|
743
|
+
cancelActiveTasksForInterruptedRuns: () => 0,
|
|
744
|
+
clearWorkspaceState: (args) => {
|
|
745
|
+
clearedArgs = args;
|
|
746
|
+
return { runs: 0, events: 0, queue: 0 };
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
session: {},
|
|
750
|
+
run: async () => {},
|
|
751
|
+
});
|
|
752
|
+
} catch (err) {
|
|
753
|
+
if (err?.code === 'EPERM') {
|
|
754
|
+
t.skip('network listen is not permitted in this sandbox');
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
throw err;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
const unscoped = await fetch(`http://127.0.0.1:${handle.port}/kill`, {
|
|
762
|
+
method: 'POST',
|
|
763
|
+
headers: { 'Content-Type': 'application/json' },
|
|
764
|
+
body: JSON.stringify({ purge: true }),
|
|
765
|
+
});
|
|
766
|
+
assert.equal(unscoped.status, 400);
|
|
767
|
+
assert.deepEqual(await unscoped.json(), { killed: false, reason: 'workspace_required' });
|
|
768
|
+
assert.equal(clearedArgs, null);
|
|
769
|
+
|
|
770
|
+
const scoped = await fetch(`http://127.0.0.1:${handle.port}/kill?workspace=docs`, {
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: { 'Content-Type': 'application/json' },
|
|
773
|
+
body: JSON.stringify({ purge: true }),
|
|
774
|
+
});
|
|
775
|
+
assert.equal(scoped.status, 202);
|
|
776
|
+
assert.equal(clearedArgs.workspace, 'docs');
|
|
777
|
+
} finally {
|
|
778
|
+
await handle.close();
|
|
779
|
+
}
|
|
780
|
+
});
|
|
781
|
+
|
|
688
782
|
test('runtime server state exposes active run identity while running', async (t) => {
|
|
689
783
|
let releaseRun;
|
|
690
784
|
const context = {
|
|
@@ -1485,6 +1579,304 @@ test('runtime server handle drains a pre-existing hydrated control request', asy
|
|
|
1485
1579
|
}
|
|
1486
1580
|
});
|
|
1487
1581
|
|
|
1582
|
+
test('POST /run compiles a workspace skill into a sequential runtime chain', async (t) => {
|
|
1583
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-skill-'));
|
|
1584
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1585
|
+
writeFileSync(join(root, '.wiki', 'skills', 'wiki-sync.md'), '---\nname: wiki-sync\nparams:\n - source\n---\nExport the source.\n\nThen ingest the files.');
|
|
1586
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1587
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1588
|
+
let startedBody = null;
|
|
1589
|
+
let handle;
|
|
1590
|
+
try {
|
|
1591
|
+
handle = await startRuntimeServer({
|
|
1592
|
+
host: '127.0.0.1', port: 0,
|
|
1593
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1594
|
+
getContext: async () => context,
|
|
1595
|
+
run: async (_context, body) => { startedBody = body; return new Promise(() => {}); },
|
|
1596
|
+
});
|
|
1597
|
+
} catch (err) {
|
|
1598
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1599
|
+
throw err;
|
|
1600
|
+
}
|
|
1601
|
+
try {
|
|
1602
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ input: '/wiki-sync docs' }) });
|
|
1603
|
+
const body = await response.json();
|
|
1604
|
+
assert.equal(response.status, 202);
|
|
1605
|
+
assert.equal(body.kind, 'skill_chain');
|
|
1606
|
+
assert.equal(body.objectives, 2);
|
|
1607
|
+
assert.equal(session.controlQueue.length, 2);
|
|
1608
|
+
assert.equal(session.controlQueue[0].status, 'running');
|
|
1609
|
+
assert.equal(session.controlQueue[1].status, 'queued');
|
|
1610
|
+
assert.equal(session.controlQueue[0].chainId, session.controlQueue[1].chainId);
|
|
1611
|
+
assert.equal('capabilityPlan' in session.controlQueue[0], false);
|
|
1612
|
+
assert.equal(session.controlQueue[0].input, '/wiki-sync source="docs"');
|
|
1613
|
+
assert.equal(session.controlQueue[0].skillExecution, 'orchestrated');
|
|
1614
|
+
assert.match(startedBody.input, /Export the source/);
|
|
1615
|
+
assert.equal(startedBody.publicInput, '/wiki-sync source="docs"');
|
|
1616
|
+
assert.equal(startedBody.skillChain.execution, 'orchestrated');
|
|
1617
|
+
assert.equal(startedBody.requireApproval, true);
|
|
1618
|
+
assert.notEqual(startedBody.autoApprove, true);
|
|
1619
|
+
assert.deepEqual(session.agentProjection.conversation, [
|
|
1620
|
+
{ role: 'user', content: '/wiki-sync docs' },
|
|
1621
|
+
]);
|
|
1622
|
+
assert.doesNotMatch(JSON.stringify(session.agentEvents), /Export the source|Then ingest the files/);
|
|
1623
|
+
} finally {
|
|
1624
|
+
context.currentAbortController?.abort();
|
|
1625
|
+
await handle.close();
|
|
1626
|
+
}
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
test('POST /turn deterministically compiles an explicit skill invocation', async (t) => {
|
|
1630
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-skill-turn-'));
|
|
1631
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1632
|
+
writeFileSync(join(root, '.wiki', 'skills', 'wiki-build.md'), '---\nname: wiki-build\nparams:\n - template\n---\nBuild the requested template.');
|
|
1633
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1634
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1635
|
+
const turns = [];
|
|
1636
|
+
let handle;
|
|
1637
|
+
try {
|
|
1638
|
+
handle = await startRuntimeServer({
|
|
1639
|
+
host: '127.0.0.1', port: 0,
|
|
1640
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1641
|
+
getContext: async () => context,
|
|
1642
|
+
run: async () => new Promise(() => {}),
|
|
1643
|
+
turn: async (_context, body) => { turns.push(body); return { ok: true }; },
|
|
1644
|
+
});
|
|
1645
|
+
} catch (err) {
|
|
1646
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1647
|
+
throw err;
|
|
1648
|
+
}
|
|
1649
|
+
try {
|
|
1650
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/turn?workspace=acme`, {
|
|
1651
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1652
|
+
body: JSON.stringify({ input: '/wiki-build overview', mode: 'agent' }),
|
|
1653
|
+
});
|
|
1654
|
+
const body = await response.json();
|
|
1655
|
+
assert.equal(response.status, 202);
|
|
1656
|
+
assert.equal(body.kind, 'skill_chain');
|
|
1657
|
+
assert.equal(body.skill, 'wiki-build');
|
|
1658
|
+
assert.equal(turns.length, 0);
|
|
1659
|
+
assert.equal(session.controlQueue.length, 1);
|
|
1660
|
+
assert.equal(session.controlQueue[0].input, '/wiki-build template="overview"');
|
|
1661
|
+
assert.deepEqual(session.agentProjection.conversation, [
|
|
1662
|
+
{ role: 'user', content: '/wiki-build overview' },
|
|
1663
|
+
]);
|
|
1664
|
+
} finally {
|
|
1665
|
+
context.currentAbortController?.abort();
|
|
1666
|
+
await handle.close();
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
|
|
1670
|
+
test('POST /turn keeps informational skill and build questions conversational', async (t) => {
|
|
1671
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-skill-question-'));
|
|
1672
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1673
|
+
writeFileSync(join(root, '.wiki', 'skills', 'new-template.md'), '---\nname: new-template\nparams:\n - family\n---\nCreate one template.');
|
|
1674
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1675
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1676
|
+
const turns = [];
|
|
1677
|
+
let handle;
|
|
1678
|
+
try {
|
|
1679
|
+
handle = await startRuntimeServer({
|
|
1680
|
+
host: '127.0.0.1', port: 0,
|
|
1681
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1682
|
+
getContext: async () => context,
|
|
1683
|
+
run: async () => new Promise(() => {}),
|
|
1684
|
+
turn: async (_context, body) => { turns.push(body); return { ok: true }; },
|
|
1685
|
+
});
|
|
1686
|
+
} catch (err) {
|
|
1687
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1688
|
+
throw err;
|
|
1689
|
+
}
|
|
1690
|
+
try {
|
|
1691
|
+
for (const input of ['Comment fonctionne new-template ?', "Qu'est-ce qu'un build ?"]) {
|
|
1692
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/turn?workspace=acme`, {
|
|
1693
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1694
|
+
body: JSON.stringify({ input, mode: 'agent' }),
|
|
1695
|
+
});
|
|
1696
|
+
const body = await response.json();
|
|
1697
|
+
assert.equal(response.status, 202);
|
|
1698
|
+
assert.equal(body.kind, 'turn');
|
|
1699
|
+
}
|
|
1700
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1701
|
+
assert.deepEqual(turns.map((body) => body.input), [
|
|
1702
|
+
'Comment fonctionne new-template ?',
|
|
1703
|
+
"Qu'est-ce qu'un build ?",
|
|
1704
|
+
]);
|
|
1705
|
+
assert.equal(session.controlQueue.length, 0);
|
|
1706
|
+
assert.equal(context.running, false);
|
|
1707
|
+
} finally {
|
|
1708
|
+
await handle.close();
|
|
1709
|
+
}
|
|
1710
|
+
});
|
|
1711
|
+
|
|
1712
|
+
test('POST /run accepts named skill arguments and deduplicates an explicit retry key', async (t) => {
|
|
1713
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-named-skill-'));
|
|
1714
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1715
|
+
writeFileSync(join(root, '.wiki', 'skills', 'deliver.md'), '---\nname: deliver\nparams:\n - template\n - polish\n---\nDeliver the output.');
|
|
1716
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1717
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1718
|
+
const persisted = new Map();
|
|
1719
|
+
let handle;
|
|
1720
|
+
try {
|
|
1721
|
+
handle = await startRuntimeServer({
|
|
1722
|
+
host: '127.0.0.1', port: 0,
|
|
1723
|
+
store: {
|
|
1724
|
+
dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [],
|
|
1725
|
+
findSkillRun: ({ idempotencyKey }) => persisted.get(idempotencyKey) ?? null,
|
|
1726
|
+
persistSkillRun: ({ idempotencyKey, chainId }) => persisted.set(idempotencyKey, chainId),
|
|
1727
|
+
},
|
|
1728
|
+
getContext: async () => context,
|
|
1729
|
+
run: async () => new Promise(() => {}),
|
|
1730
|
+
});
|
|
1731
|
+
} catch (err) {
|
|
1732
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1733
|
+
throw err;
|
|
1734
|
+
}
|
|
1735
|
+
try {
|
|
1736
|
+
const request = () => fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
1737
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1738
|
+
body: JSON.stringify({ input: '/deliver', skillName: 'deliver', skillArguments: { template: 'Quarterly report' }, idempotencyKey: 'retry-1' }),
|
|
1739
|
+
});
|
|
1740
|
+
const first = await (await request()).json();
|
|
1741
|
+
const second = await (await request()).json();
|
|
1742
|
+
assert.equal(first.accepted, true);
|
|
1743
|
+
assert.equal(second.deduplicated, true);
|
|
1744
|
+
assert.equal(second.chainId, first.chainId);
|
|
1745
|
+
assert.equal(session.controlQueue.length, 1);
|
|
1746
|
+
assert.equal('input' in first.items[0], false);
|
|
1747
|
+
assert.equal('objectives' in first, false);
|
|
1748
|
+
assert.equal(session.controlQueue[0].input, '/deliver template="Quarterly report"');
|
|
1749
|
+
} finally {
|
|
1750
|
+
context.currentAbortController?.abort();
|
|
1751
|
+
await handle.close();
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
|
|
1755
|
+
test('POST /run does not unlock a reserved skill with a mismatched skillName', async (t) => {
|
|
1756
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-reserved-skill-'));
|
|
1757
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1758
|
+
writeFileSync(join(root, '.wiki', 'skills', 'status.md'), '---\nname: status\nparams: []\n---\nRun the workspace status skill.');
|
|
1759
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1760
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1761
|
+
let received = null;
|
|
1762
|
+
let handle;
|
|
1763
|
+
try {
|
|
1764
|
+
handle = await startRuntimeServer({
|
|
1765
|
+
host: '127.0.0.1', port: 0,
|
|
1766
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1767
|
+
getContext: async () => context,
|
|
1768
|
+
run: async (_context, body) => { received = body; },
|
|
1769
|
+
});
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1772
|
+
throw err;
|
|
1773
|
+
}
|
|
1774
|
+
try {
|
|
1775
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
1776
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1777
|
+
body: JSON.stringify({ input: '/status', skillName: 'anything' }),
|
|
1778
|
+
});
|
|
1779
|
+
const body = await response.json();
|
|
1780
|
+
assert.equal(response.status, 202);
|
|
1781
|
+
assert.notEqual(body.kind, 'skill_chain');
|
|
1782
|
+
assert.equal(session.controlQueue.length, 0);
|
|
1783
|
+
await new Promise((resolve) => setImmediate(resolve));
|
|
1784
|
+
assert.equal(received.input, '/status');
|
|
1785
|
+
} finally {
|
|
1786
|
+
context.currentAbortController?.abort();
|
|
1787
|
+
await handle.close();
|
|
1788
|
+
}
|
|
1789
|
+
});
|
|
1790
|
+
|
|
1791
|
+
test('structured named arguments unlock a reserved skill only when input names the same skill', async (t) => {
|
|
1792
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-structured-reserved-'));
|
|
1793
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1794
|
+
writeFileSync(join(root, '.wiki', 'skills', 'status.md'), '---\nname: status\n---\nRun workspace status.');
|
|
1795
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1796
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1797
|
+
let handle;
|
|
1798
|
+
try {
|
|
1799
|
+
handle = await startRuntimeServer({
|
|
1800
|
+
host: '127.0.0.1', port: 0,
|
|
1801
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1802
|
+
getContext: async () => context,
|
|
1803
|
+
run: async () => new Promise(() => {}),
|
|
1804
|
+
});
|
|
1805
|
+
} catch (err) {
|
|
1806
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1807
|
+
throw err;
|
|
1808
|
+
}
|
|
1809
|
+
try {
|
|
1810
|
+
const post = (input) => fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
1811
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1812
|
+
body: JSON.stringify({ input, skillName: 'status', skillArguments: {} }),
|
|
1813
|
+
});
|
|
1814
|
+
const mismatch = await post('/deliver');
|
|
1815
|
+
assert.equal(mismatch.status, 400);
|
|
1816
|
+
assert.equal((await mismatch.json()).code, 'skill_name_mismatch');
|
|
1817
|
+
const explicit = await post('/status');
|
|
1818
|
+
assert.equal(explicit.status, 202);
|
|
1819
|
+
assert.equal((await explicit.json()).skill, 'status');
|
|
1820
|
+
} finally {
|
|
1821
|
+
context.currentAbortController?.abort();
|
|
1822
|
+
await handle.close();
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
|
|
1826
|
+
test('legacy placeholders consume only their own parameter', async (t) => {
|
|
1827
|
+
const root = mkdtempSync(join(tmpdir(), 'runtime-partial-placeholder-'));
|
|
1828
|
+
mkdirSync(join(root, '.wiki', 'skills'), { recursive: true });
|
|
1829
|
+
writeFileSync(join(root, '.wiki', 'skills', 'partial.md'), '---\nname: partial\nparams:\n - a\n - b\n---\nUse {a}.\n\nThen process the remaining input.');
|
|
1830
|
+
const session = { workspace: 'acme', workspacePath: root, controlQueue: [] };
|
|
1831
|
+
const context = { workspace: 'acme', session, running: false, currentAbortController: null };
|
|
1832
|
+
let handle;
|
|
1833
|
+
try {
|
|
1834
|
+
handle = await startRuntimeServer({
|
|
1835
|
+
host: '127.0.0.1', port: 0,
|
|
1836
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1837
|
+
getContext: async () => context,
|
|
1838
|
+
run: async () => new Promise(() => {}),
|
|
1839
|
+
});
|
|
1840
|
+
} catch (err) {
|
|
1841
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1842
|
+
throw err;
|
|
1843
|
+
}
|
|
1844
|
+
try {
|
|
1845
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/run?workspace=acme`, {
|
|
1846
|
+
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
1847
|
+
body: JSON.stringify({ input: '/partial alpha beta' }),
|
|
1848
|
+
});
|
|
1849
|
+
const body = await response.json();
|
|
1850
|
+
assert.equal(response.status, 202);
|
|
1851
|
+
assert.deepEqual(body.deprecatedPlaceholders, ['a']);
|
|
1852
|
+
assert.equal(session.controlQueue.length, 2);
|
|
1853
|
+
for (const item of session.controlQueue) assert.equal(item.input, '/partial a="alpha" b="beta"');
|
|
1854
|
+
} finally {
|
|
1855
|
+
context.currentAbortController?.abort();
|
|
1856
|
+
await handle.close();
|
|
1857
|
+
}
|
|
1858
|
+
});
|
|
1859
|
+
|
|
1860
|
+
test('POST /control cancel_item returns a readable non-error for a non-queued item', async (t) => {
|
|
1861
|
+
const session = { workspace: 'acme', controlQueue: [{ id: 'done-item', status: 'done', input: 'done' }] };
|
|
1862
|
+
let handle;
|
|
1863
|
+
try {
|
|
1864
|
+
handle = await startRuntimeServer({
|
|
1865
|
+
host: '127.0.0.1', port: 0,
|
|
1866
|
+
store: { dbPath: ':memory:', getState: () => ({ status: 'idle', plan: [], queue: [], approvals: [] }), listEvents: () => [] },
|
|
1867
|
+
getContext: async () => ({ workspace: 'acme', session, running: false }), run: async () => {},
|
|
1868
|
+
});
|
|
1869
|
+
} catch (err) {
|
|
1870
|
+
if (err?.code === 'EPERM') { t.skip('network listen is not permitted in this sandbox'); return; }
|
|
1871
|
+
throw err;
|
|
1872
|
+
}
|
|
1873
|
+
try {
|
|
1874
|
+
const response = await fetch(`http://127.0.0.1:${handle.port}/control?workspace=acme`, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ action: 'cancel_item', id: 'done-item' }) });
|
|
1875
|
+
assert.equal(response.status, 200);
|
|
1876
|
+
assert.deepEqual(await response.json().then(({ cancelled, reason }) => ({ cancelled, reason })), { cancelled: false, reason: 'not_queued' });
|
|
1877
|
+
} finally { await handle.close(); }
|
|
1878
|
+
});
|
|
1879
|
+
|
|
1488
1880
|
test('runtime server exposes config profile list and switch endpoints', async (t) => {
|
|
1489
1881
|
const context = {
|
|
1490
1882
|
workspace: 'acme',
|