blun-king-cli 9.1.526 → 9.1.536
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 +67 -0
- package/LIESMICH.txt +36 -1
- package/README.md +35 -1
- package/bin/agent-resume-snapshot.cjs +31 -0
- package/bin/assistant-message-offload-policy.cjs +21 -2
- package/bin/codebase-search-runtime.cjs +23 -0
- package/bin/empty-response-retry-policy.cjs +29 -0
- package/bin/fredrik-glm-provider.cjs +256 -0
- package/bin/history-offload-pressure-policy.cjs +33 -0
- package/bin/programmatic-context-isolation.cjs +25 -0
- package/bin/programmatic-tool-runtime.mjs +301 -0
- package/bin/skill-activation-performance-policy.cjs +9 -0
- package/bin/structured-subagent-output.cjs +252 -0
- package/bin/telegram-direct-focus-policy.cjs +25 -1
- package/bin/todo-list-turn-policy.cjs +111 -1
- package/bin/tool-result-offload-policy.cjs +29 -0
- package/bin/turn-thinking-policy.cjs +6 -15
- package/bin/turn-tool-performance-policy.cjs +5 -4
- package/bin/user-message-offload-policy.cjs +10 -1
- package/blun.mjs +709 -127
- package/codebase-index/README.md +70 -0
- package/codebase-index/codebase_index.py +358 -0
- package/fredrik-glm-profile.toml.example +26 -0
- package/package.json +25 -3
- package/scripts/check-active-work-steer-regression.js +46 -0
- package/scripts/check-codebase-search-packaging-regression.js +92 -0
- package/scripts/check-copy-command-regression.js +74 -0
- package/scripts/check-current-turn-read-pin-mutation-regression.js +72 -0
- package/scripts/check-current-turn-read-pin-regression.js +94 -0
- package/scripts/check-deepseek-native-max-regression.js +49 -0
- package/scripts/check-empty-response-effort-downgrade-regression.js +48 -0
- package/scripts/check-fredrik-glm-mutation-regression.js +18 -0
- package/scripts/check-fredrik-glm-regression.js +169 -0
- package/scripts/check-history-pressure-offload-regression.js +77 -0
- package/scripts/check-programmatic-context-isolation-regression.js +193 -0
- package/scripts/check-programmatic-tool-regression.js +294 -0
- package/scripts/check-resume-replay-regression.js +2 -0
- package/scripts/check-startup-swarm-command-regression.js +24 -0
- package/scripts/check-structured-subagent-output-regression.js +331 -0
- package/scripts/check-telegram-direct-work-resume-regression.js +53 -0
- package/scripts/check-todo-progress-regression.js +416 -0
- package/scripts/check-tool-schema-capacity-regression.js +40 -0
- package/scripts/programmatic-tool-runtime.test.mjs +365 -0
- package/scripts/structured-subagent-output.test.cjs +170 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const vm = require('node:vm');
|
|
7
|
+
|
|
8
|
+
const rootArg = process.argv.find((value) => value.startsWith('--root='));
|
|
9
|
+
const packageRoot = rootArg ? path.resolve(rootArg.slice('--root='.length)) : path.resolve(__dirname, '..');
|
|
10
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
11
|
+
|
|
12
|
+
function extractFunction(name) {
|
|
13
|
+
const start = bundle.indexOf(`function ${name}(`);
|
|
14
|
+
assert.notEqual(start, -1, `${name} must exist`);
|
|
15
|
+
const open = bundle.indexOf('{', start);
|
|
16
|
+
let depth = 0;
|
|
17
|
+
for (let index = open; index < bundle.length; index += 1) {
|
|
18
|
+
if (bundle[index] === '{') depth += 1;
|
|
19
|
+
if (bundle[index] === '}') depth -= 1;
|
|
20
|
+
if (depth === 0) return bundle.slice(start, index + 1);
|
|
21
|
+
}
|
|
22
|
+
assert.fail(`${name} must have a complete function body`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
assert.match(
|
|
26
|
+
bundle,
|
|
27
|
+
/name:\s*"copy"[\s\S]{0,180}descriptionKey:\s*"command\.copy\.description"[\s\S]{0,180}availability:\s*"always"/,
|
|
28
|
+
'/copy must be registered and available while a turn is running',
|
|
29
|
+
);
|
|
30
|
+
assert.match(bundle, /case "copy":\s*await handleCopyCommand\(host\);\s*return;/, '/copy must dispatch to its handler');
|
|
31
|
+
assert.match(bundle, /await copyTextToClipboard\(text\);/, '/copy must use the existing cross-platform clipboard helper');
|
|
32
|
+
|
|
33
|
+
const selectorSource = extractFunction('findLatestAssistantResponse');
|
|
34
|
+
const findLatestAssistantResponse = vm.runInNewContext(`(${selectorSource})`);
|
|
35
|
+
assert.equal(
|
|
36
|
+
findLatestAssistantResponse([
|
|
37
|
+
{ kind: 'assistant', content: ' previous answer ' },
|
|
38
|
+
{ kind: 'user', content: 'question' },
|
|
39
|
+
{ kind: 'assistant', content: ' ' },
|
|
40
|
+
]),
|
|
41
|
+
' previous answer ',
|
|
42
|
+
'the selector must ignore user and empty assistant entries while preserving response bytes',
|
|
43
|
+
);
|
|
44
|
+
assert.equal(findLatestAssistantResponse([{ kind: 'user', content: 'question' }]), undefined);
|
|
45
|
+
|
|
46
|
+
const requiredUiStrings = [
|
|
47
|
+
'Copy the latest assistant response to the clipboard.',
|
|
48
|
+
'Copied the latest response to the clipboard.',
|
|
49
|
+
'There is no response to copy yet.',
|
|
50
|
+
'Could not copy the latest response: {error}',
|
|
51
|
+
'Neueste Assistentenantwort in die Zwischenablage übernehmen.',
|
|
52
|
+
'Die neueste Antwort wurde in die Zwischenablage übernommen.',
|
|
53
|
+
'Es ist noch keine Antwort zum Kopieren verfügbar.',
|
|
54
|
+
'Die neueste Antwort konnte nicht kopiert werden: {error}',
|
|
55
|
+
'Copiar la respuesta más reciente del asistente al portapapeles.',
|
|
56
|
+
'La respuesta más reciente se copió al portapapeles.',
|
|
57
|
+
'Todavía no hay ninguna respuesta que copiar.',
|
|
58
|
+
'No se pudo copiar la respuesta más reciente: {error}',
|
|
59
|
+
'Copier la réponse la plus récente de l’assistant dans le presse-papiers.',
|
|
60
|
+
'La réponse la plus récente a été copiée dans le presse-papiers.',
|
|
61
|
+
'Aucune réponse à copier pour le moment.',
|
|
62
|
+
'Impossible de copier la réponse la plus récente : {error}',
|
|
63
|
+
'Kopiera assistentens senaste svar till urklipp.',
|
|
64
|
+
'Det senaste svaret kopierades till urklipp.',
|
|
65
|
+
'Det finns inget svar att kopiera ännu.',
|
|
66
|
+
'Det gick inte att kopiera det senaste svaret: {error}',
|
|
67
|
+
'Zkopírovat nejnovější odpověď asistenta do schránky.',
|
|
68
|
+
'Nejnovější odpověď byla zkopírována do schránky.',
|
|
69
|
+
'Zatím není k dispozici žádná odpověď ke zkopírování.',
|
|
70
|
+
'Nejnovější odpověď se nepodařilo zkopírovat: {error}',
|
|
71
|
+
];
|
|
72
|
+
for (const text of requiredUiStrings) assert.ok(bundle.includes(text), `missing released UI text: ${text}`);
|
|
73
|
+
|
|
74
|
+
process.stdout.write('Copy command regression gate: PASS\n');
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
|
|
9
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
10
|
+
const gatePath = path.join(packageRoot, 'scripts', 'check-current-turn-read-pin-regression.js');
|
|
11
|
+
const sourcePolicy = fs.readFileSync(
|
|
12
|
+
path.join(packageRoot, 'bin', 'tool-result-offload-policy.cjs'),
|
|
13
|
+
'utf8',
|
|
14
|
+
);
|
|
15
|
+
const sourceBundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
16
|
+
|
|
17
|
+
const mutations = [
|
|
18
|
+
{
|
|
19
|
+
name: 'policy helper removed',
|
|
20
|
+
policy: sourcePolicy.replace(
|
|
21
|
+
'function currentTurnReadToolResultIds(messages) {',
|
|
22
|
+
'function removedCurrentTurnReadToolResultIds(messages) {',
|
|
23
|
+
),
|
|
24
|
+
bundle: sourceBundle,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: 'injections accepted as user turns',
|
|
28
|
+
policy: sourcePolicy.replace(
|
|
29
|
+
"message?.role === 'user' && message.origin?.kind === 'user'",
|
|
30
|
+
"message?.role === 'user'",
|
|
31
|
+
),
|
|
32
|
+
bundle: sourceBundle,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'Read pin filter removed',
|
|
36
|
+
policy: sourcePolicy,
|
|
37
|
+
bundle: sourceBundle.replace(
|
|
38
|
+
' && !pinnedReadIds.has(message.toolCallId)',
|
|
39
|
+
'',
|
|
40
|
+
),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'current user instruction no longer skipped',
|
|
44
|
+
policy: sourcePolicy,
|
|
45
|
+
bundle: sourceBundle.replace(
|
|
46
|
+
'if (historyIndex === currentTurnStart) continue;',
|
|
47
|
+
'if (false && historyIndex === currentTurnStart) continue;',
|
|
48
|
+
),
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (const mutation of mutations) {
|
|
53
|
+
if (mutation.policy === sourcePolicy && mutation.bundle === sourceBundle) {
|
|
54
|
+
throw new Error(`mutation marker missing: ${mutation.name}`);
|
|
55
|
+
}
|
|
56
|
+
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-read-pin-mutation-'));
|
|
57
|
+
try {
|
|
58
|
+
fs.mkdirSync(path.join(tempRoot, 'bin'), { recursive: true });
|
|
59
|
+
fs.writeFileSync(path.join(tempRoot, 'bin', 'tool-result-offload-policy.cjs'), mutation.policy);
|
|
60
|
+
fs.writeFileSync(path.join(tempRoot, 'blun.mjs'), mutation.bundle);
|
|
61
|
+
const result = spawnSync(process.execPath, [gatePath], {
|
|
62
|
+
cwd: packageRoot,
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
env: { ...process.env, BLUN_PACKAGE_UNDER_TEST: tempRoot },
|
|
65
|
+
});
|
|
66
|
+
if (result.status === 0) throw new Error(`gate accepted mutation: ${mutation.name}`);
|
|
67
|
+
} finally {
|
|
68
|
+
fs.rmSync(tempRoot, { recursive: true, force: true });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
process.stdout.write(`current-turn-read-pin-mutation-regression PASS (${mutations.length} mutations rejected)\n`);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const packageRoot = process.env.BLUN_PACKAGE_UNDER_TEST
|
|
8
|
+
? path.resolve(process.env.BLUN_PACKAGE_UNDER_TEST)
|
|
9
|
+
: path.resolve(__dirname, '..');
|
|
10
|
+
const policy = require(path.join(packageRoot, 'bin', 'tool-result-offload-policy.cjs'));
|
|
11
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
12
|
+
|
|
13
|
+
function assert(condition, message) {
|
|
14
|
+
if (!condition) throw new Error(`CURRENT_TURN_READ_PIN_REGRESSION: ${message}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
assert(
|
|
18
|
+
typeof policy.currentTurnReadToolResultIds === 'function',
|
|
19
|
+
'currentTurnReadToolResultIds policy is missing',
|
|
20
|
+
);
|
|
21
|
+
assert(
|
|
22
|
+
typeof policy.currentUserTurnStartIndex === 'function',
|
|
23
|
+
'currentUserTurnStartIndex policy is missing',
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const history = [
|
|
27
|
+
{ role: 'user', origin: { kind: 'user' }, content: [{ type: 'text', text: 'old task' }] },
|
|
28
|
+
{
|
|
29
|
+
role: 'assistant',
|
|
30
|
+
toolCalls: [{ id: 'old-read', name: 'Read', arguments: JSON.stringify({ path: 'old.md' }) }],
|
|
31
|
+
},
|
|
32
|
+
{ role: 'tool', toolCallId: 'old-read', content: [{ type: 'text', text: 'old result' }] },
|
|
33
|
+
{ role: 'user', origin: { kind: 'injection' }, content: [{ type: 'text', text: 'hook' }] },
|
|
34
|
+
{ role: 'user', origin: { kind: 'user' }, content: [{ type: 'text', text: 'current task' }] },
|
|
35
|
+
{
|
|
36
|
+
role: 'assistant',
|
|
37
|
+
toolCalls: [
|
|
38
|
+
{ id: 'task-read', name: 'Read', arguments: JSON.stringify({ path: 'task.md' }) },
|
|
39
|
+
{ id: 'shell-call', name: 'Bash', arguments: JSON.stringify({ command: 'pwd' }) },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{ role: 'tool', toolCallId: 'task-read', content: [{ type: 'text', text: 'complete task' }] },
|
|
43
|
+
{ role: 'tool', toolCallId: 'shell-call', content: [{ type: 'text', text: '/workspace' }] },
|
|
44
|
+
{
|
|
45
|
+
role: 'assistant',
|
|
46
|
+
toolCalls: [{ id: 'source-read', name: 'Read', arguments: JSON.stringify({ path: 'source.js' }) }],
|
|
47
|
+
},
|
|
48
|
+
{ role: 'tool', toolCallId: 'source-read', content: [{ type: 'text', text: 'source slice' }] },
|
|
49
|
+
{ role: 'assistant', toolCalls: [] },
|
|
50
|
+
{ role: 'tool', toolCallId: 'unrelated-result', content: [{ type: 'text', text: 'other' }] },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const pinned = policy.currentTurnReadToolResultIds(history);
|
|
54
|
+
assert(pinned instanceof Set, 'policy must return a Set');
|
|
55
|
+
assert(pinned.has('task-read'), 'complete task Read was not pinned');
|
|
56
|
+
assert(pinned.has('source-read'), 'current-turn source Read was not pinned');
|
|
57
|
+
assert(!pinned.has('old-read'), 'Read from a completed older user turn stayed pinned');
|
|
58
|
+
assert(!pinned.has('shell-call'), 'non-Read tool result was pinned');
|
|
59
|
+
assert(
|
|
60
|
+
policy.currentUserTurnStartIndex(history) === 4,
|
|
61
|
+
'latest real user message was not selected as the current turn start',
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const withoutUserTurn = policy.currentTurnReadToolResultIds([
|
|
65
|
+
{ role: 'user', origin: { kind: 'injection' }, content: [{ type: 'text', text: 'hook' }] },
|
|
66
|
+
{
|
|
67
|
+
role: 'assistant',
|
|
68
|
+
toolCalls: [{ id: 'injected-read', name: 'Read', arguments: '{}' }],
|
|
69
|
+
},
|
|
70
|
+
]);
|
|
71
|
+
assert(withoutUserTurn.size === 0, 'injection-only history must not create a pinned user turn');
|
|
72
|
+
assert(
|
|
73
|
+
policy.currentUserTurnStartIndex(withoutUserTurn) === -1,
|
|
74
|
+
'injection-only history must not create a current user turn',
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
assert(
|
|
78
|
+
bundle.includes('const pinnedReadIds = currentTurnReadToolResultIds(history);'),
|
|
79
|
+
'batch offload does not calculate current-turn Read pins',
|
|
80
|
+
);
|
|
81
|
+
assert(
|
|
82
|
+
bundle.includes('&& !pinnedReadIds.has(message.toolCallId)'),
|
|
83
|
+
'batch offload does not exclude pinned Read results',
|
|
84
|
+
);
|
|
85
|
+
assert(
|
|
86
|
+
bundle.includes('const currentTurnStart = currentUserTurnStartIndex(history);'),
|
|
87
|
+
'user-message offload does not locate the current turn',
|
|
88
|
+
);
|
|
89
|
+
assert(
|
|
90
|
+
bundle.includes('if (historyIndex === currentTurnStart) continue;'),
|
|
91
|
+
'current user instruction can still be offloaded during its own turn',
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
process.stdout.write('current-turn-read-pin-regression PASS\n');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const fs = require('node:fs');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const packageRoot = path.resolve(__dirname, '..');
|
|
5
|
+
const bundlePath = path.join(packageRoot, 'blun.mjs');
|
|
6
|
+
|
|
7
|
+
function checkBundle(bundle) {
|
|
8
|
+
const required = [
|
|
9
|
+
'max: "Max"',
|
|
10
|
+
'if (effort !== void 0) this.opts.onSessionOnlySelect(effort);',
|
|
11
|
+
'if (this.isBlunSwarmSelected) this.opts.onBlunSwarmSelect?.();',
|
|
12
|
+
'if (effort !== void 0) this.opts.onSelect(effort);',
|
|
13
|
+
'state.swarmMode && state.swarmModeEntry === "effort"',
|
|
14
|
+
];
|
|
15
|
+
for (const marker of required) {
|
|
16
|
+
if (!bundle.includes(marker)) throw new Error(`missing native-max marker: ${marker}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const forbidden = [
|
|
20
|
+
'max: "Swarm"',
|
|
21
|
+
'usesLegacyServerStageScale',
|
|
22
|
+
];
|
|
23
|
+
for (const marker of forbidden) {
|
|
24
|
+
if (bundle.includes(marker)) throw new Error(`legacy max-to-swarm path remains: ${marker}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const bundle = fs.readFileSync(bundlePath, 'utf8');
|
|
29
|
+
checkBundle(bundle);
|
|
30
|
+
|
|
31
|
+
const mutations = [
|
|
32
|
+
bundle.replace('max: "Max"', 'max: "Swarm"'),
|
|
33
|
+
bundle.replace(
|
|
34
|
+
'if (effort !== void 0) this.opts.onSelect(effort);',
|
|
35
|
+
'if (effort === "max" && this.usesLegacyServerStageScale) this.opts.onBlunSwarmSelect();',
|
|
36
|
+
),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
for (const [index, mutation] of mutations.entries()) {
|
|
40
|
+
let rejected = false;
|
|
41
|
+
try {
|
|
42
|
+
checkBundle(mutation);
|
|
43
|
+
} catch {
|
|
44
|
+
rejected = true;
|
|
45
|
+
}
|
|
46
|
+
if (!rejected) throw new Error(`mutation ${index + 1} escaped native-max gate`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
console.log('DeepSeek native max regression gate passed (2 mutations rejected).');
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const fs = require('node:fs');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
|
|
8
|
+
const packageRoot = process.env.BLUN_PACKAGE_UNDER_TEST
|
|
9
|
+
? path.resolve(process.env.BLUN_PACKAGE_UNDER_TEST)
|
|
10
|
+
: path.resolve(__dirname, '..');
|
|
11
|
+
const policy = require(path.join(packageRoot, 'bin', 'empty-response-retry-policy.cjs'));
|
|
12
|
+
const bundle = fs.readFileSync(path.join(packageRoot, 'blun.mjs'), 'utf8');
|
|
13
|
+
|
|
14
|
+
const exhausted = (thinkingEffort) => policy.nextThinkingEffortForExhaustedEmpty({
|
|
15
|
+
emptyResponseKind: 'length',
|
|
16
|
+
maxCompletionTokens: 32_768,
|
|
17
|
+
completionTokens: 32_768,
|
|
18
|
+
thinkingEffort,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
assert.equal(exhausted('max'), 'high');
|
|
22
|
+
assert.equal(exhausted('xhigh'), 'high');
|
|
23
|
+
assert.equal(exhausted('high'), 'low');
|
|
24
|
+
assert.equal(exhausted('low'), 'off');
|
|
25
|
+
assert.equal(policy.nextThinkingEffortForExhaustedEmpty({
|
|
26
|
+
emptyResponseKind: 'length',
|
|
27
|
+
maxCompletionTokens: 32_768,
|
|
28
|
+
completionTokens: 2_000,
|
|
29
|
+
thinkingEffort: 'max',
|
|
30
|
+
}), undefined, 'a non-exhausted response must not downgrade thinking');
|
|
31
|
+
assert.equal(policy.nextThinkingEffortForExhaustedEmpty({
|
|
32
|
+
emptyResponseKind: 'stop',
|
|
33
|
+
maxCompletionTokens: 32_768,
|
|
34
|
+
completionTokens: 32_768,
|
|
35
|
+
thinkingEffort: 'max',
|
|
36
|
+
}), undefined, 'a stop response must not be treated as budget exhaustion');
|
|
37
|
+
|
|
38
|
+
for (const needle of [
|
|
39
|
+
'thinkingEffortRetry = nextThinkingEffortForExhaustedEmpty({',
|
|
40
|
+
'thinkingEffort: input.llm.thinkingEffort',
|
|
41
|
+
'const requestProvider = params.thinkingEffortRetry === void 0 ? this.provider : this.provider.withThinking(params.thinkingEffortRetry);',
|
|
42
|
+
'provider: requestProvider,',
|
|
43
|
+
'retryThinkingEffort',
|
|
44
|
+
]) {
|
|
45
|
+
assert.ok(bundle.includes(needle), `empty-response retry wiring missing: ${needle}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
process.stdout.write('empty-response-effort-downgrade-regression PASS\n');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
const { REQUIRED_BUNDLE_MARKERS, validateBundleText } = require('./check-fredrik-glm-regression.js');
|
|
7
|
+
|
|
8
|
+
const bundlePath = path.resolve(__dirname, '..', 'blun.mjs');
|
|
9
|
+
const source = fs.readFileSync(bundlePath, 'utf8');
|
|
10
|
+
assert.deepEqual(validateBundleText(source), []);
|
|
11
|
+
|
|
12
|
+
for (const marker of REQUIRED_BUNDLE_MARKERS) {
|
|
13
|
+
const mutated = source.replaceAll(marker, `MUTATED_${marker.length}`);
|
|
14
|
+
assert.notEqual(mutated, source, `mutation marker missing from source: ${marker}`);
|
|
15
|
+
assert.notDeepEqual(validateBundleText(mutated), [], `gate accepted mutation: ${marker}`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.stdout.write(`fredrik-glm-mutation-regression: ok (${REQUIRED_BUNDLE_MARKERS.length} mutations)\n`);
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const assert = require('node:assert/strict');
|
|
4
|
+
const fs = require('node:fs');
|
|
5
|
+
const os = require('node:os');
|
|
6
|
+
const path = require('node:path');
|
|
7
|
+
const { spawnSync } = require('node:child_process');
|
|
8
|
+
const policy = require('../bin/fredrik-glm-provider.cjs');
|
|
9
|
+
|
|
10
|
+
const PACKAGE_ROOT = path.resolve(__dirname, '..');
|
|
11
|
+
const BUNDLE_PATH = path.join(PACKAGE_ROOT, 'blun.mjs');
|
|
12
|
+
const TEMPLATE_PATH = path.join(PACKAGE_ROOT, 'fredrik-glm-profile.toml.example');
|
|
13
|
+
|
|
14
|
+
const REQUIRED_BUNDLE_MARKERS = Object.freeze([
|
|
15
|
+
'literal("openai_compatible")',
|
|
16
|
+
'apiKeyFile: string().optional()',
|
|
17
|
+
'providerName !== FREDRIK_GLM_PROVIDER_ID || !isConfiguredFredrikGlmAlias(model, alias)',
|
|
18
|
+
'type: providerConfig.type',
|
|
19
|
+
'apiKey: readFredrikGlmApiKeyFile(provider)',
|
|
20
|
+
'openAICompatible: true',
|
|
21
|
+
'const controls = fredrikGlmRequestControls(effectiveThinkingEffort ?? "max")',
|
|
22
|
+
'requestExtraBody["thinking"] = controls.thinking',
|
|
23
|
+
'requestExtraBody["reasoning_effort"] = controls.reasoning_effort',
|
|
24
|
+
'name: "model"',
|
|
25
|
+
'await handleModelCommand(host, args)',
|
|
26
|
+
'performModelSwitch(host, selection.alias, selection.thinking, false)',
|
|
27
|
+
'...configuredFredrikGlmModels(models)',
|
|
28
|
+
'[FREDRIK_GLM_PROVIDER_ID]: fredrikGlm',
|
|
29
|
+
'model: status.model ?? BLUN_KING_MODEL_ALIAS',
|
|
30
|
+
'!isAllowedFredrikRuntimeAlias(resumedStatus.model ?? BLUN_KING_MODEL_ALIAS, this.state.appState.availableModels)',
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const FORBIDDEN_BUNDLE_MARKERS = Object.freeze([
|
|
34
|
+
'..."model" in patch && patch.model?.trim().length ? { model: BLUN_KING_MODEL_ALIAS }',
|
|
35
|
+
'..."activeResponderModel" in patch && patch.activeResponderModel != null ? { activeResponderModel: BLUN_KING_MODEL_ALIAS }',
|
|
36
|
+
]);
|
|
37
|
+
|
|
38
|
+
function validateBundleText(source) {
|
|
39
|
+
const failures = [];
|
|
40
|
+
for (const marker of REQUIRED_BUNDLE_MARKERS) {
|
|
41
|
+
if (!source.includes(marker)) failures.push(`missing:${marker}`);
|
|
42
|
+
}
|
|
43
|
+
for (const marker of FORBIDDEN_BUNDLE_MARKERS) {
|
|
44
|
+
if (source.includes(marker)) failures.push(`forbidden:${marker}`);
|
|
45
|
+
}
|
|
46
|
+
const setSessionBlock = source.slice(source.indexOf('\n\tasync setSession(session, options = {}) {'), source.indexOf('\n\tasync syncRuntimeState(', source.indexOf('\n\tasync setSession(session, options = {}) {')));
|
|
47
|
+
if (!setSessionBlock.includes('!isAllowedFredrikRuntimeAlias(resumedStatus.model ?? BLUN_KING_MODEL_ALIAS')) failures.push('resume-missing-profile-guard');
|
|
48
|
+
const reloadBlock = source.slice(source.indexOf('\n\tasync reloadCurrentSessionView('), source.indexOf('\n\tasync restoreSession(', source.indexOf('\n\tasync reloadCurrentSessionView(')));
|
|
49
|
+
if (reloadBlock.includes('session.setModel(BLUN_KING_MODEL_ALIAS')) failures.push('reload-forces-king');
|
|
50
|
+
return failures;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function exactModels() {
|
|
54
|
+
return {
|
|
55
|
+
'fredrik/glm-5.3-flash': {
|
|
56
|
+
provider: policy.PROVIDER_ID,
|
|
57
|
+
model: 'glm-5.3-flash',
|
|
58
|
+
displayName: 'GLM 5.3 Flash',
|
|
59
|
+
maxContextSize: 1000000,
|
|
60
|
+
maxOutputSize: 128000,
|
|
61
|
+
capabilities: ['tool_use', 'always_thinking'],
|
|
62
|
+
supportEfforts: ['low', 'high', 'max'],
|
|
63
|
+
defaultEffort: 'max',
|
|
64
|
+
},
|
|
65
|
+
'fredrik/glm-5.3': {
|
|
66
|
+
provider: policy.PROVIDER_ID,
|
|
67
|
+
model: 'glm-5.3',
|
|
68
|
+
displayName: 'GLM 5.3',
|
|
69
|
+
maxContextSize: 1000000,
|
|
70
|
+
maxOutputSize: 128000,
|
|
71
|
+
capabilities: ['tool_use', 'always_thinking'],
|
|
72
|
+
supportEfforts: ['low', 'high', 'max'],
|
|
73
|
+
defaultEffort: 'max',
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function run() {
|
|
79
|
+
const source = fs.readFileSync(BUNDLE_PATH, 'utf8');
|
|
80
|
+
assert.deepEqual(validateBundleText(source), []);
|
|
81
|
+
|
|
82
|
+
assert.equal(policy.BASE_URL, 'https://api.z.ai/api/coding/paas/v4');
|
|
83
|
+
assert.deepEqual([...policy.MODEL_EFFORTS], ['low', 'high', 'max']);
|
|
84
|
+
assert.deepEqual(policy.requestControls('low'), { thinking: { type: 'enabled' }, reasoning_effort: 'low' });
|
|
85
|
+
assert.deepEqual(policy.requestControls('max'), { thinking: { type: 'enabled' }, reasoning_effort: 'max' });
|
|
86
|
+
assert.throws(() => policy.requestControls('off'), /only low, high, or max/u);
|
|
87
|
+
assert.equal(policy.normalizeBaseUrl(`${policy.BASE_URL}/`), policy.BASE_URL);
|
|
88
|
+
for (const bad of ['http://api.z.ai/api/coding/paas/v4', 'https://api.z.ai/api/paas/v4', 'https://api.z.ai/api/coding/paas/v4?key=x', 'https://example.com/api/coding/paas/v4']) {
|
|
89
|
+
assert.throws(() => policy.normalizeBaseUrl(bad), /approved api\.z\.ai Coding endpoint/u);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const models = exactModels();
|
|
93
|
+
assert.deepEqual(Object.keys(policy.configuredModels(models)), ['fredrik/glm-5.3-flash', 'fredrik/glm-5.3']);
|
|
94
|
+
assert.equal(policy.isAllowedRuntimeAlias('blun/king', models), true);
|
|
95
|
+
assert.equal(policy.isAllowedRuntimeAlias('fredrik/glm-5.3', models), true);
|
|
96
|
+
assert.equal(policy.isAllowedRuntimeAlias('werner/glm-5.3', models), false);
|
|
97
|
+
const mutated = structuredClone(models);
|
|
98
|
+
mutated['fredrik/glm-5.3'].supportEfforts = ['low', 'max'];
|
|
99
|
+
assert.equal(policy.isConfiguredAlias('fredrik/glm-5.3', mutated['fredrik/glm-5.3']), false);
|
|
100
|
+
|
|
101
|
+
const providers = {
|
|
102
|
+
[policy.PROVIDER_ID]: {
|
|
103
|
+
type: policy.PROVIDER_TYPE,
|
|
104
|
+
baseUrl: policy.BASE_URL,
|
|
105
|
+
apiKeyFile: 'secrets/zai-api-key.txt',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
assert.equal(policy.configuredProvider(providers), providers[policy.PROVIDER_ID]);
|
|
109
|
+
const diagnostic = policy.safeDiagnostic('fredrik/glm-5.3', models, providers, true);
|
|
110
|
+
assert.deepEqual(diagnostic, {
|
|
111
|
+
alias: 'fredrik/glm-5.3',
|
|
112
|
+
alias_source: 'profile-config',
|
|
113
|
+
provider: 'fredrik:zai',
|
|
114
|
+
provider_type: 'openai_compatible',
|
|
115
|
+
model: 'glm-5.3',
|
|
116
|
+
base_host: 'api.z.ai',
|
|
117
|
+
credential_source: 'profile-file',
|
|
118
|
+
credential_present: true,
|
|
119
|
+
credential_fingerprint: null,
|
|
120
|
+
wire_format: 'openai-chat-completions',
|
|
121
|
+
health: 'untested',
|
|
122
|
+
});
|
|
123
|
+
assert.equal(JSON.stringify(diagnostic).includes('zai-api-key.txt'), false);
|
|
124
|
+
|
|
125
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'blun-fredrik-glm-'));
|
|
126
|
+
try {
|
|
127
|
+
const secretDir = path.join(root, 'secrets');
|
|
128
|
+
fs.mkdirSync(secretDir);
|
|
129
|
+
const secretPath = path.join(secretDir, 'zai-api-key.txt');
|
|
130
|
+
const canary = 'secret-canary-never-print';
|
|
131
|
+
fs.writeFileSync(secretPath, `${canary}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
132
|
+
const read = policy.readApiKeyFile(providers[policy.PROVIDER_ID], { BLUN_HOME: root });
|
|
133
|
+
assert.equal(read, canary);
|
|
134
|
+
assert.throws(() => policy.readApiKeyFile({ ...providers[policy.PROVIDER_ID], apiKey: canary }, { BLUN_HOME: root }), /only api_key_file/u);
|
|
135
|
+
assert.throws(() => policy.readApiKeyFile({ ...providers[policy.PROVIDER_ID], apiKeyFile: '..\\outside.txt' }, { BLUN_HOME: root }), /escapes the profile/u);
|
|
136
|
+
fs.writeFileSync(secretPath, `${canary}\nsecond\n`, 'utf8');
|
|
137
|
+
assert.throws(() => policy.readApiKeyFile(providers[policy.PROVIDER_ID], { BLUN_HOME: root }), /exactly one non-empty line/u);
|
|
138
|
+
fs.writeFileSync(secretPath, `${canary}\n`, 'utf8');
|
|
139
|
+
if (process.platform === 'win32') {
|
|
140
|
+
const aclMutation = spawnSync('icacls.exe', [secretPath, '/grant', '*S-1-1-0:(M)'], { encoding: 'utf8', windowsHide: true });
|
|
141
|
+
assert.equal(aclMutation.status, 0, 'ACL mutation setup failed');
|
|
142
|
+
assert.throws(() => policy.readApiKeyFile(providers[policy.PROVIDER_ID], { BLUN_HOME: root }), /ACL is too permissive/u);
|
|
143
|
+
}
|
|
144
|
+
const missingProvider = { ...providers[policy.PROVIDER_ID], apiKeyFile: 'secrets/zai-api-key.txt' };
|
|
145
|
+
fs.rmSync(secretPath, { force: true });
|
|
146
|
+
assert.throws(() => policy.readApiKeyFile(missingProvider, { BLUN_HOME: root }), (error) => {
|
|
147
|
+
assert.equal(error.message.includes(root), false);
|
|
148
|
+
return /could not be resolved safely/u.test(error.message);
|
|
149
|
+
});
|
|
150
|
+
} finally {
|
|
151
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const template = fs.readFileSync(TEMPLATE_PATH, 'utf8');
|
|
155
|
+
assert.match(template, /\[providers\."fredrik:zai"\]/u);
|
|
156
|
+
assert.match(template, /api_key_file = "secrets\/zai-api-key\.txt"/u);
|
|
157
|
+
assert.equal(template.includes('api_key ='), false);
|
|
158
|
+
assert.equal(template.includes('default_model'), false);
|
|
159
|
+
assert.equal(template.includes('werner'), false);
|
|
160
|
+
assert.equal(template.includes('manfred'), false);
|
|
161
|
+
|
|
162
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(PACKAGE_ROOT, 'package.json'), 'utf8'));
|
|
163
|
+
assert.equal(pkg.version, '9.1.536');
|
|
164
|
+
process.stdout.write('fredrik-glm-regression: ok\n');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
module.exports = { REQUIRED_BUNDLE_MARKERS, validateBundleText };
|
|
168
|
+
|
|
169
|
+
if (require.main === module) run();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const path = require('node:path');
|
|
6
|
+
|
|
7
|
+
const packageRoot = process.env.BLUN_PACKAGE_UNDER_TEST
|
|
8
|
+
? path.resolve(process.env.BLUN_PACKAGE_UNDER_TEST)
|
|
9
|
+
: path.resolve(__dirname, '..');
|
|
10
|
+
const userPolicy = require(path.join(packageRoot, 'bin', 'user-message-offload-policy.cjs'));
|
|
11
|
+
const assistantPolicy = require(path.join(packageRoot, 'bin', 'assistant-message-offload-policy.cjs'));
|
|
12
|
+
const skillPolicy = require(path.join(packageRoot, 'bin', 'skill-activation-performance-policy.cjs'));
|
|
13
|
+
|
|
14
|
+
function textMessage(role, text, origin) {
|
|
15
|
+
return { role, content: [{ type: 'text', text }], toolCalls: [], ...(origin ? { origin } : {}) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const smallHistory = Array.from({ length: 25 }, (_, index) => textMessage(
|
|
19
|
+
index === 0 ? 'user' : 'assistant',
|
|
20
|
+
'x'.repeat(5_000),
|
|
21
|
+
index === 0
|
|
22
|
+
? { kind: 'skill_activation', skillName: 'test-skill', skillPath: 'C:/test/SKILL.md' }
|
|
23
|
+
: undefined,
|
|
24
|
+
));
|
|
25
|
+
const smallChars = userPolicy.historyTextChars(smallHistory);
|
|
26
|
+
assert.equal(userPolicy.shouldOffloadHistoricalUserMessage({
|
|
27
|
+
historyIndex: 0,
|
|
28
|
+
historyLength: smallHistory.length,
|
|
29
|
+
historyChars: smallChars,
|
|
30
|
+
textChars: 5_000,
|
|
31
|
+
}), false, 'message 21 alone must not trigger user-message offload');
|
|
32
|
+
assert.equal(assistantPolicy.shouldOffloadHistoricalAssistantMessage({
|
|
33
|
+
historyIndex: 1,
|
|
34
|
+
historyLength: smallHistory.length,
|
|
35
|
+
historyChars: smallChars,
|
|
36
|
+
textChars: 5_000,
|
|
37
|
+
}), false, 'message 21 alone must not trigger assistant-message offload');
|
|
38
|
+
assert.equal(assistantPolicy.shouldCompactHistoricalAssistantToolNarration({
|
|
39
|
+
historyIndex: 1,
|
|
40
|
+
historyLength: smallHistory.length,
|
|
41
|
+
historyChars: smallChars,
|
|
42
|
+
textChars: 500,
|
|
43
|
+
toolCallCount: 1,
|
|
44
|
+
completedToolCallCount: 1,
|
|
45
|
+
}), false, 'message 21 alone must not compact completed tool narration');
|
|
46
|
+
assert.strictEqual(
|
|
47
|
+
skillPolicy.compactHistoricalSkillActivations(smallHistory),
|
|
48
|
+
smallHistory,
|
|
49
|
+
'message 21 alone must not compact active skill instructions',
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const largeHistory = Array.from({ length: 70 }, (_, index) => textMessage(
|
|
53
|
+
index === 0 ? 'user' : 'assistant',
|
|
54
|
+
'y'.repeat(20_000),
|
|
55
|
+
index === 0
|
|
56
|
+
? { kind: 'skill_activation', skillName: 'test-skill', skillPath: 'C:/test/SKILL.md' }
|
|
57
|
+
: undefined,
|
|
58
|
+
));
|
|
59
|
+
const largeChars = userPolicy.historyTextChars(largeHistory);
|
|
60
|
+
assert.ok(largeChars >= 1_000_000);
|
|
61
|
+
assert.equal(userPolicy.shouldOffloadHistoricalUserMessage({
|
|
62
|
+
historyIndex: 0,
|
|
63
|
+
historyLength: largeHistory.length,
|
|
64
|
+
historyChars: largeChars,
|
|
65
|
+
textChars: 20_000,
|
|
66
|
+
}), true, 'real history pressure must retain archival offload');
|
|
67
|
+
assert.equal(assistantPolicy.shouldOffloadHistoricalAssistantMessage({
|
|
68
|
+
historyIndex: 1,
|
|
69
|
+
historyLength: largeHistory.length,
|
|
70
|
+
historyChars: largeChars,
|
|
71
|
+
textChars: 20_000,
|
|
72
|
+
}), true, 'real history pressure must retain assistant archival offload');
|
|
73
|
+
const compactedSkills = skillPolicy.compactHistoricalSkillActivations(largeHistory);
|
|
74
|
+
assert.notStrictEqual(compactedSkills, largeHistory);
|
|
75
|
+
assert.match(compactedSkills[0].content[0].text, /Earlier skill activation compacted/u);
|
|
76
|
+
|
|
77
|
+
process.stdout.write('history-pressure-offload-regression PASS\n');
|