nubos-pilot 1.2.4 → 1.3.1
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/CHANGELOG.md +17 -1
- package/README.md +2 -1
- package/SECURITY.md +3 -4
- package/bin/np-tools/_commands.cjs +3 -0
- package/bin/np-tools/_elision-proxy-entry.cjs +13 -0
- package/bin/np-tools/elision-bench.cjs +67 -0
- package/bin/np-tools/elision-get.cjs +48 -0
- package/bin/np-tools/elision-get.test.cjs +66 -0
- package/bin/np-tools/learnings.cjs +1 -1
- package/bin/np-tools/loop-run-round.cjs +25 -11
- package/bin/np-tools/plan-milestone.cjs +1 -0
- package/bin/np-tools/research-phase.cjs +1 -1
- package/bin/np-tools/resolve-model.cjs +55 -1
- package/bin/np-tools/resolve-model.test.cjs +139 -0
- package/bin/np-tools/security.cjs +1 -1
- package/bin/np-tools/spawn-headless.cjs +155 -3
- package/bin/np-tools/spawn-headless.test.cjs +108 -58
- package/bin/np-tools/spawn-offhost.cjs +93 -0
- package/bin/np-tools/spawn-offhost.test.cjs +38 -0
- package/lib/agents.cjs +16 -2
- package/lib/cache-align.cjs +78 -0
- package/lib/cache-align.test.cjs +69 -0
- package/lib/compress.cjs +495 -0
- package/lib/compress.test.cjs +267 -0
- package/lib/config-defaults.cjs +39 -0
- package/lib/config-schema.cjs +45 -5
- package/lib/elision-bench.cjs +409 -0
- package/lib/elision-bench.test.cjs +89 -0
- package/lib/elision-proxy.cjs +158 -0
- package/lib/elision-proxy.test.cjs +243 -0
- package/lib/elision.cjs +163 -0
- package/lib/elision.test.cjs +143 -0
- package/lib/learnings/extract.cjs +4 -4
- package/lib/learnings/extract.test.cjs +8 -8
- package/lib/model-providers.cjs +118 -0
- package/lib/model-providers.test.cjs +85 -0
- package/lib/nubosloop.cjs +1 -1
- package/lib/output-steering.cjs +68 -0
- package/lib/output-steering.test.cjs +74 -0
- package/lib/researcher-swarm.cjs +14 -3
- package/lib/runtime/agent-loop.cjs +94 -0
- package/lib/runtime/agent-loop.test.cjs +240 -0
- package/lib/runtime/dispatch.cjs +174 -0
- package/lib/runtime/dispatch.test.cjs +207 -0
- package/lib/runtime/preflight.cjs +68 -0
- package/lib/runtime/preflight.test.cjs +62 -0
- package/lib/runtime/providers/openai-compat.cjs +103 -0
- package/lib/runtime/providers/openai-compat.test.cjs +112 -0
- package/lib/runtime/tools/index.cjs +447 -0
- package/lib/runtime/tools/index.test.cjs +254 -0
- package/lib/schemas/data/elision-entry.v1.json +16 -0
- package/lib/security/review.cjs +4 -4
- package/lib/security/review.test.cjs +6 -6
- package/lib/token-cost.cjs +46 -0
- package/lib/token-cost.test.cjs +42 -0
- package/np-tools.cjs +3 -0
- package/package.json +1 -1
- package/workflows/add-tests.md +41 -0
- package/workflows/architect-phase.md +19 -0
- package/workflows/discuss-phase.md +29 -10
- package/workflows/execute-phase.md +93 -4
- package/workflows/plan-phase.md +57 -16
- package/workflows/research-phase.md +45 -0
- package/workflows/scan-codebase.md +21 -3
- package/workflows/validate-phase.md +30 -13
- package/workflows/verify-work.md +17 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('node:fs');
|
|
4
|
+
const { NubosPilotError } = require('../../lib/core.cjs');
|
|
5
|
+
const { dispatchOffHost } = require('../../lib/runtime/dispatch.cjs');
|
|
6
|
+
|
|
7
|
+
function _usage() {
|
|
8
|
+
process.stderr.write(
|
|
9
|
+
'Usage: np-tools.cjs spawn-offhost --agent <name> (--task <str> | --task-file <path>) '
|
|
10
|
+
+ '[--cwd <dir>] [--phase P] [--plan P] [--task-id T] [--max-iterations N] [--allow-bash] [--read-only] [--no-audit]\n',
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function _parse(argv) {
|
|
15
|
+
const out = { allowBash: false, readOnly: false };
|
|
16
|
+
const a = argv.slice();
|
|
17
|
+
while (a.length) {
|
|
18
|
+
const f = a.shift();
|
|
19
|
+
if (f === '--agent') out.agent = a.shift();
|
|
20
|
+
else if (f === '--task') out.task = a.shift();
|
|
21
|
+
else if (f === '--task-file') out.taskFile = a.shift();
|
|
22
|
+
else if (f === '--phase') out.phase = a.shift();
|
|
23
|
+
else if (f === '--plan') out.plan = a.shift();
|
|
24
|
+
else if (f === '--task-id') out.taskId = a.shift();
|
|
25
|
+
else if (f === '--max-iterations') out.maxIterations = Number(a.shift());
|
|
26
|
+
else if (f === '--cwd') out.cwd = a.shift();
|
|
27
|
+
else if (f === '--output-schema') out.outputSchema = a.shift();
|
|
28
|
+
else if (f === '--allow-bash') out.allowBash = true;
|
|
29
|
+
else if (f === '--read-only') out.readOnly = true;
|
|
30
|
+
else if (f === '--no-audit') out.skipAudit = true;
|
|
31
|
+
}
|
|
32
|
+
return out;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function run(argv) {
|
|
36
|
+
const args = Array.isArray(argv) ? argv.slice() : process.argv.slice(3);
|
|
37
|
+
if (!args.length || args[0] === '--help') { _usage(); return 1; }
|
|
38
|
+
const parsed = _parse(args);
|
|
39
|
+
|
|
40
|
+
let task = parsed.task;
|
|
41
|
+
if (parsed.taskFile) {
|
|
42
|
+
try { task = fs.readFileSync(parsed.taskFile, 'utf-8'); }
|
|
43
|
+
catch { process.stderr.write(JSON.stringify({ code: 'spawn-offhost-task-file-unreadable', file: require('node:path').basename(parsed.taskFile) }) + '\n'); return 1; }
|
|
44
|
+
}
|
|
45
|
+
if (!parsed.agent || typeof task !== 'string') { _usage(); return 1; }
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const result = await dispatchOffHost({
|
|
49
|
+
agent: parsed.agent,
|
|
50
|
+
task,
|
|
51
|
+
cwd: parsed.cwd || process.cwd(),
|
|
52
|
+
phase: parsed.phase,
|
|
53
|
+
plan: parsed.plan,
|
|
54
|
+
taskId: parsed.taskId,
|
|
55
|
+
maxIterations: parsed.maxIterations,
|
|
56
|
+
allowBash: parsed.allowBash,
|
|
57
|
+
readOnly: parsed.readOnly,
|
|
58
|
+
skipAudit: parsed.skipAudit,
|
|
59
|
+
outputSchema: parsed.outputSchema,
|
|
60
|
+
});
|
|
61
|
+
if (result && result.metrics_recorded === false) {
|
|
62
|
+
process.stderr.write('spawn-offhost: metrics row was not recorded (telemetry only; run succeeded)\n');
|
|
63
|
+
}
|
|
64
|
+
if (result && result.rule9 && result.rule9.ok === false) {
|
|
65
|
+
process.stderr.write('spawn-offhost: Rule-9 violation (' + (result.rule9.violation || result.rule9.error)
|
|
66
|
+
+ ') — the agent did not satisfy the search bar. Do NOT commit this output as-is; re-run or route back to the agent.\n');
|
|
67
|
+
}
|
|
68
|
+
if (result && result.capability && result.capability.ok === false) {
|
|
69
|
+
const c = result.capability;
|
|
70
|
+
process.stderr.write('spawn-offhost: the model advertised ' + c.toolsAdvertised
|
|
71
|
+
+ ' tool(s) but made zero tool calls — the provider/model likely does NOT support OpenAI function/tool-calling. '
|
|
72
|
+
+ (c.mutating
|
|
73
|
+
? 'This agent edits files; off-host it produced NO changes. Route it to a tool-calling-capable model or keep it native.'
|
|
74
|
+
: 'If this agent was expected to inspect the workspace, its output may be ungrounded — verify before relying on it.')
|
|
75
|
+
+ '\n');
|
|
76
|
+
}
|
|
77
|
+
process.stdout.write(JSON.stringify(result) + '\n');
|
|
78
|
+
return 0;
|
|
79
|
+
} catch (err) {
|
|
80
|
+
if (err && err.name === 'NubosPilotError') {
|
|
81
|
+
process.stderr.write(JSON.stringify({ code: err.code, message: err.message, details: err.details }) + '\n');
|
|
82
|
+
} else {
|
|
83
|
+
process.stderr.write(String((err && err.stack) || err) + '\n');
|
|
84
|
+
}
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { run, _parse };
|
|
90
|
+
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
run(process.argv.slice(2)).then((code) => process.exit(code || 0));
|
|
93
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const { test } = require('node:test');
|
|
2
|
+
const assert = require('node:assert/strict');
|
|
3
|
+
|
|
4
|
+
const subcmd = require('./spawn-offhost.cjs');
|
|
5
|
+
|
|
6
|
+
function _capture(fn) {
|
|
7
|
+
const out = []; const err = [];
|
|
8
|
+
const oo = process.stdout.write.bind(process.stdout);
|
|
9
|
+
const oe = process.stderr.write.bind(process.stderr);
|
|
10
|
+
process.stdout.write = (c) => { out.push(String(c)); return true; };
|
|
11
|
+
process.stderr.write = (c) => { err.push(String(c)); return true; };
|
|
12
|
+
return Promise.resolve(fn()).then((rc) => {
|
|
13
|
+
process.stdout.write = oo; process.stderr.write = oe;
|
|
14
|
+
return { rc, stdout: out.join(''), stderr: err.join('') };
|
|
15
|
+
}, (e) => { process.stdout.write = oo; process.stderr.write = oe; throw e; });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('SOH-1: _parse reads agent/task, the boolean flags, --cwd and --no-audit', () => {
|
|
19
|
+
const p = subcmd._parse(['--agent', 'np-executor', '--task', 'do x', '--allow-bash', '--max-iterations', '5', '--cwd', '/wt', '--no-audit']);
|
|
20
|
+
assert.equal(p.agent, 'np-executor');
|
|
21
|
+
assert.equal(p.task, 'do x');
|
|
22
|
+
assert.equal(p.allowBash, true);
|
|
23
|
+
assert.equal(p.readOnly, false);
|
|
24
|
+
assert.equal(p.maxIterations, 5);
|
|
25
|
+
assert.equal(p.cwd, '/wt');
|
|
26
|
+
assert.equal(p.skipAudit, true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('SOH-2: missing args prints usage and returns 1', async () => {
|
|
30
|
+
const { rc, stderr } = await _capture(() => subcmd.run([]));
|
|
31
|
+
assert.equal(rc, 1);
|
|
32
|
+
assert.match(stderr, /Usage:/);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('SOH-3: --agent without a task returns 1', async () => {
|
|
36
|
+
const { rc } = await _capture(() => subcmd.run(['--agent', 'np-executor']));
|
|
37
|
+
assert.equal(rc, 1);
|
|
38
|
+
});
|
package/lib/agents.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('node:fs');
|
|
4
4
|
const path = require('node:path');
|
|
5
|
-
const { extractFrontmatter } = require('./frontmatter.cjs');
|
|
5
|
+
const { extractFrontmatter, stripFrontmatter } = require('./frontmatter.cjs');
|
|
6
6
|
const { NubosPilotError, findProjectRoot } = require('./core.cjs');
|
|
7
7
|
|
|
8
8
|
const REQUIRED = ['name', 'description', 'tier', 'tools'];
|
|
@@ -60,7 +60,7 @@ function validateAgentFrontmatter(fm, agentName) {
|
|
|
60
60
|
|
|
61
61
|
const AGENT_NAME_RE = /^[a-zA-Z0-9_-]+$/;
|
|
62
62
|
|
|
63
|
-
function
|
|
63
|
+
function _resolveAgentPath(name, cwd) {
|
|
64
64
|
if (typeof name !== 'string' || !AGENT_NAME_RE.test(name)) {
|
|
65
65
|
throw new NubosPilotError(
|
|
66
66
|
'agent-invalid-name',
|
|
@@ -83,10 +83,23 @@ function _loadAgentFromDisk(name, cwd) {
|
|
|
83
83
|
{ name, path: candidates[0], tried: candidates },
|
|
84
84
|
);
|
|
85
85
|
}
|
|
86
|
+
return found;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _loadAgentFromDisk(name, cwd) {
|
|
90
|
+
const found = _resolveAgentPath(name, cwd);
|
|
86
91
|
const { frontmatter } = extractFrontmatter(fs.readFileSync(found, 'utf-8'));
|
|
87
92
|
return validateAgentFrontmatter(frontmatter, name);
|
|
88
93
|
}
|
|
89
94
|
|
|
95
|
+
function loadAgentSource(name, cwd) {
|
|
96
|
+
const found = _resolveAgentPath(name, cwd);
|
|
97
|
+
const raw = fs.readFileSync(found, 'utf-8');
|
|
98
|
+
const { frontmatter } = extractFrontmatter(raw);
|
|
99
|
+
validateAgentFrontmatter(frontmatter, name);
|
|
100
|
+
return { frontmatter, body: stripFrontmatter(raw), path: found };
|
|
101
|
+
}
|
|
102
|
+
|
|
90
103
|
function loadAgent(name, cwd) {
|
|
91
104
|
const fm = _loadAgentFromDisk(name, cwd);
|
|
92
105
|
if (fm.module === true) {
|
|
@@ -143,6 +156,7 @@ module.exports = {
|
|
|
143
156
|
validateAgentFrontmatter,
|
|
144
157
|
loadAgent,
|
|
145
158
|
loadAgentModule,
|
|
159
|
+
loadAgentSource,
|
|
146
160
|
listAgents,
|
|
147
161
|
getAgentSkills,
|
|
148
162
|
AGENT_NAME_RE,
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const UUID_RE = /\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i;
|
|
4
|
+
const ISO_RE = /\b\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/;
|
|
5
|
+
const JWT_RE = /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/;
|
|
6
|
+
const HEX_RE = /\b[0-9a-f]{32,64}\b/i;
|
|
7
|
+
const VOLATILE = Object.freeze([
|
|
8
|
+
['uuid', UUID_RE],
|
|
9
|
+
['iso_datetime', ISO_RE],
|
|
10
|
+
['jwt', JWT_RE],
|
|
11
|
+
['hex_hash', HEX_RE],
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
function _systemText(system) {
|
|
15
|
+
if (typeof system === 'string') return system;
|
|
16
|
+
if (Array.isArray(system)) {
|
|
17
|
+
return system.map((b) => (b && typeof b.text === 'string' ? b.text : '')).join('\n');
|
|
18
|
+
}
|
|
19
|
+
return '';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function detectVolatile(system) {
|
|
23
|
+
const text = _systemText(system);
|
|
24
|
+
const findings = [];
|
|
25
|
+
for (const [kind, re] of VOLATILE) {
|
|
26
|
+
const m = text.match(re);
|
|
27
|
+
if (m) findings.push({ kind, sample_len: m[0].length });
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _sortKeys(v) {
|
|
33
|
+
if (Array.isArray(v)) return v.map(_sortKeys);
|
|
34
|
+
if (v && typeof v === 'object') {
|
|
35
|
+
const out = {};
|
|
36
|
+
for (const k of Object.keys(v).sort()) out[k] = _sortKeys(v[k]);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
return v;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function _byCodepoint(a, b) {
|
|
43
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeTools(tools) {
|
|
47
|
+
return tools
|
|
48
|
+
.map(_sortKeys)
|
|
49
|
+
.map((t) => ({ t, name: String(t && t.name), body: JSON.stringify(t) }))
|
|
50
|
+
.sort((a, b) => _byCodepoint(a.name, b.name) || _byCodepoint(a.body, b.body))
|
|
51
|
+
.map((x) => x.t);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function _anyCacheControl(system, tools) {
|
|
55
|
+
if (Array.isArray(system) && system.some((b) => b && b.cache_control)) return true;
|
|
56
|
+
if (Array.isArray(tools) && tools.some((t) => t && t.cache_control)) return true;
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function alignAnthropicBody(body) {
|
|
61
|
+
const findings = detectVolatile(body && body.system);
|
|
62
|
+
if (!body || !Array.isArray(body.tools) || body.tools.length === 0) {
|
|
63
|
+
return { body, findings, applied: false };
|
|
64
|
+
}
|
|
65
|
+
if (_anyCacheControl(body.system, body.tools)) {
|
|
66
|
+
return { body, findings, applied: false };
|
|
67
|
+
}
|
|
68
|
+
const tools = normalizeTools(body.tools);
|
|
69
|
+
const last = tools.length - 1;
|
|
70
|
+
tools[last] = Object.assign({}, tools[last], { cache_control: { type: 'ephemeral' } });
|
|
71
|
+
return { body: Object.assign({}, body, { tools }), findings, applied: true };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
detectVolatile,
|
|
76
|
+
normalizeTools,
|
|
77
|
+
alignAnthropicBody,
|
|
78
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { test } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
|
|
6
|
+
const ca = require('./cache-align.cjs');
|
|
7
|
+
|
|
8
|
+
test('CA-1: detectVolatile flags UUID/ISO-date/JWT/hex in the system prompt', () => {
|
|
9
|
+
const sys = 'Run id 550e8400-e29b-41d4-a716-446655440000 at 2026-06-23T10:00:00Z '
|
|
10
|
+
+ 'token eyJhbGc.eyJzdWI.sig hash d41d8cd98f00b204e9800998ecf8427e';
|
|
11
|
+
const kinds = ca.detectVolatile(sys).map((f) => f.kind).sort();
|
|
12
|
+
assert.deepEqual(kinds, ['hex_hash', 'iso_datetime', 'jwt', 'uuid']);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('CA-2: detectVolatile reads an array-form system prompt and is clean when stable', () => {
|
|
16
|
+
assert.deepEqual(ca.detectVolatile([{ type: 'text', text: 'You are a stable agent.' }]), []);
|
|
17
|
+
assert.deepEqual(ca.detectVolatile('plain stable instructions'), []);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('CA-3: normalizeTools sorts tools by name and recursively sorts schema keys; arrays keep order', () => {
|
|
21
|
+
const tools = [
|
|
22
|
+
{ name: 'zeta', description: 'z', input_schema: { type: 'object', required: ['b', 'a'], properties: { y: {}, x: {} } } },
|
|
23
|
+
{ name: 'alpha', description: 'a', input_schema: { properties: { n: {} }, type: 'object' } },
|
|
24
|
+
];
|
|
25
|
+
const out = ca.normalizeTools(tools);
|
|
26
|
+
assert.deepEqual(out.map((t) => t.name), ['alpha', 'zeta']);
|
|
27
|
+
assert.deepEqual(Object.keys(out[1]), ['description', 'input_schema', 'name']);
|
|
28
|
+
assert.deepEqual(Object.keys(out[1].input_schema.properties), ['x', 'y']);
|
|
29
|
+
assert.deepEqual(out[1].input_schema.required, ['b', 'a']);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('CA-4: alignAnthropicBody adds exactly one ephemeral breakpoint on the last tool and is idempotent', () => {
|
|
33
|
+
const body = { system: 'sys', tools: [{ name: 'b' }, { name: 'a' }], messages: [] };
|
|
34
|
+
const r1 = ca.alignAnthropicBody(body);
|
|
35
|
+
assert.equal(r1.applied, true);
|
|
36
|
+
assert.deepEqual(r1.body.tools.map((t) => t.name), ['a', 'b']);
|
|
37
|
+
assert.equal(r1.body.tools[0].cache_control, undefined);
|
|
38
|
+
assert.deepEqual(r1.body.tools[1].cache_control, { type: 'ephemeral' });
|
|
39
|
+
const r2 = ca.alignAnthropicBody(r1.body);
|
|
40
|
+
assert.equal(r2.applied, false, 'second pass is a no-op — breakpoint already present');
|
|
41
|
+
assert.equal(JSON.stringify(r2.body), JSON.stringify(r1.body), 'byte-identical second pass');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('CA-4b: normalizeTools is byte-deterministic regardless of input order (codepoint, not locale)', () => {
|
|
45
|
+
const a = [{ name: 'get_A' }, { name: 'get-a' }, { name: 'get_a' }, { name: 'tool10' }, { name: 'tool2' }];
|
|
46
|
+
const b = a.slice().reverse();
|
|
47
|
+
assert.equal(JSON.stringify(ca.normalizeTools(a)), JSON.stringify(ca.normalizeTools(b)), 'order-independent, stable bytes');
|
|
48
|
+
assert.deepEqual(ca.normalizeTools(a).map((t) => t.name), ['get-a', 'get_A', 'get_a', 'tool10', 'tool2']);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('CA-4c: duplicate/degenerate names still order deterministically via body tiebreak', () => {
|
|
52
|
+
const x = [{ name: 'x', id: 2 }, { name: 'x', id: 1 }];
|
|
53
|
+
assert.equal(JSON.stringify(ca.normalizeTools(x)), JSON.stringify(ca.normalizeTools(x.slice().reverse())));
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('CA-5: customer-placement-wins — a caller-set cache_control is never disturbed', () => {
|
|
57
|
+
const body = { tools: [{ name: 'z', cache_control: { type: 'ephemeral' } }, { name: 'a' }], messages: [] };
|
|
58
|
+
const r = ca.alignAnthropicBody(body);
|
|
59
|
+
assert.equal(r.applied, false);
|
|
60
|
+
assert.deepEqual(r.body.tools.map((t) => t.name), ['z', 'a'], 'tool order untouched when caller manages caching');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('CA-6: no tools → no normalization, detection still runs', () => {
|
|
64
|
+
const body = { system: 'id 550e8400-e29b-41d4-a716-446655440000', messages: [] };
|
|
65
|
+
const r = ca.alignAnthropicBody(body);
|
|
66
|
+
assert.equal(r.applied, false);
|
|
67
|
+
assert.equal(r.body, body);
|
|
68
|
+
assert.deepEqual(r.findings.map((f) => f.kind), ['uuid']);
|
|
69
|
+
});
|