@inneranimalmedia/agentsam-sdk 2.5.0 → 2.6.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/AGENTSAM.md +55 -0
- package/README.md +12 -8
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- package/docs/CLI_SHELL.md +163 -53
- package/docs/PLATFORM_RUNTIME_EVENTS.md +48 -0
- package/docs/RELEASES.md +16 -7
- package/docs/SOURCE_ARCHITECTURE.md +58 -0
- package/docs/TEST_TIERS.md +26 -0
- package/migrations/runtime/0001_cli_runtime.sql +298 -0
- package/package.json +45 -12
- package/packages/agentsam-repository/README.md +15 -0
- package/packages/agentsam-repository/package.json +25 -0
- package/packages/agentsam-repository/src/contracts.js +113 -0
- package/packages/agentsam-repository/src/index.js +3 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/cloudflare-persistence.js +14 -24
- package/{src/lib → packages/agentsam-repository/src}/merkle/index.js +1 -0
- package/{src/lib → packages/agentsam-repository/src}/merkle/persistence.js +6 -4
- package/{src/lib → packages/agentsam-repository/src}/merkle/policy.js +1 -0
- package/packages/agentsam-repository/test/contracts.test.mjs +40 -0
- package/packages/agentsam-repository/test/git-context.test.mjs +24 -0
- package/{test/merkle.test.mjs → packages/agentsam-repository/test/merkle-core.test.mjs} +2 -32
- package/{test → packages/agentsam-repository/test}/merkle-persistence.test.mjs +11 -6
- package/packages/connectors/cloudflare/package.json +10 -0
- package/packages/connectors/cloudflare/src/index.js +127 -0
- package/packages/connectors/cloudflare/src/owner.js +76 -0
- package/packages/connectors/cloudflare/src/routes.js +223 -0
- package/packages/connectors/cloudflare/src/vault.js +80 -0
- package/packages/connectors/cloudflare/tests/connector.test.mjs +44 -0
- package/packages/identity/package.json +2 -2
- package/packages/identity/src/contracts/auth-config.js +18 -7
- package/packages/identity/tests/auth-config.test.mjs +9 -5
- package/packages/identity/tests/oauth-credentials.test.mjs +4 -4
- package/protocol/COMPANY_REPOSITORY_GRAPH_V1.md +91 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +2 -0
- package/protocol/MERKLE_PERSISTENCE_V2.md +40 -0
- package/protocol/README.md +1 -0
- package/protocol/capabilities/cloudflare-cpu-audit-input.schema.json +19 -0
- package/protocol/capabilities/cloudflare-cpu-profile-input.schema.json +13 -0
- package/protocol/capabilities/cloudflare-wrangler-native-input.schema.json +19 -0
- package/protocol/capabilities/manifest.json +47 -0
- package/protocol/context/context-budget.schema.json +10 -15
- package/protocol/context/context-item.schema.json +4 -5
- package/protocol/context/resolved-context-pack.schema.json +19 -14
- package/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- package/protocol/repository/repository-contract.schema.json +24 -0
- package/protocol/repository/repository-dependency.schema.json +24 -0
- package/protocol/repository/repository-identity.schema.json +17 -0
- package/protocol/rpc/v1/common.proto +16 -0
- package/protocol/rpc/v1/errors.proto +35 -0
- package/protocol/rpc/v1/knowledge.proto +77 -0
- package/services/knowledge/package-lock.json +333 -0
- package/services/knowledge/package.json +5 -1
- package/skills/agentsam-cloudflare-workers/SKILL.md +53 -0
- package/skills/agentsam-cloudflare-workers/references/cpu-profiling.md +16 -0
- package/skills/agentsam-cloudflare-workers/references/errors-and-observability.md +29 -0
- package/skills/agentsam-cloudflare-workers/references/wrangler-native-map.md +28 -0
- package/skills/catalog.json +18 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +353 -0
- package/src/capabilities/repository-snapshot.js +3 -3
- package/src/cli.js +118 -31
- package/src/cloudflare/cpu-profile.js +115 -0
- package/src/cloudflare/index.js +14 -0
- package/src/cloudflare/wrangler.js +132 -0
- package/src/commands/account-auth.js +47 -0
- package/src/commands/cloudflare.js +58 -0
- package/src/commands/connections.js +93 -0
- package/src/commands/context-economics.js +129 -0
- package/src/commands/context.js +1 -1
- package/src/commands/db.js +20 -3
- package/src/commands/deploy.js +39 -3
- package/src/commands/env.js +90 -0
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +2 -5
- package/src/commands/knowledge.js +12 -4
- package/src/commands/merkle-persist.js +30 -11
- package/src/commands/merkle.js +1 -1
- package/src/commands/models.js +149 -46
- package/src/commands/ollama.js +26 -0
- package/src/commands/preferences.js +130 -61
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +5 -3
- package/src/commands/shell.js +568 -119
- package/src/commands/tunnel.js +2 -2
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +68 -6
- package/src/context/index.js +3 -1
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +44 -12
- package/src/errors/contract.js +236 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +23 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +68 -2
- package/src/knowledge/service/auth.js +13 -0
- package/src/knowledge/service/grpc-client.js +115 -0
- package/src/knowledge/service/grpc-codec.js +237 -0
- package/src/knowledge/service/grpc-server.js +83 -0
- package/src/knowledge/service/job-engine.js +248 -0
- package/src/knowledge/service/server.js +87 -135
- package/src/knowledge/source.js +1 -1
- package/src/lib/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/cli-preferences.js +55 -24
- package/src/lib/deploy/git-guard.js +69 -0
- package/src/lib/deploy/health.js +57 -0
- package/src/lib/deploy/local-studio.js +283 -0
- package/src/lib/deploy/secret-scan.js +65 -0
- package/src/lib/deploy-receipt/index.js +2 -2
- package/src/lib/detect-context.js +2 -2
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/knowledge-docker.js +6 -3
- package/src/lib/local-sessions.js +148 -0
- package/src/lib/local-status.js +1 -1
- package/src/lib/project-config.js +1 -1
- package/src/lib/provider-credentials.js +183 -0
- package/src/lib/scaffold/templates/worker-api/index.js +101 -20
- package/src/lib/scaffold/wizards/worker-api.js +27 -11
- package/src/lib/slash-commands.js +23 -16
- package/src/local/migrations.js +93 -0
- package/src/local/runtime-store.js +141 -0
- package/src/local/sqlite.js +2 -0
- package/src/local-pty/server.js +113 -51
- package/src/models/catalog.js +135 -0
- package/src/models/discovery.js +292 -0
- package/src/models/index.js +7 -0
- package/src/providers/anthropic-messages.js +192 -0
- package/src/providers/cloudflare-chat.js +183 -0
- package/src/providers/factory.js +69 -0
- package/src/providers/gemini-generate-content.js +208 -0
- package/src/providers/index.js +10 -0
- package/src/providers/ollama-chat.js +148 -0
- package/src/providers/openai-responses.js +426 -0
- package/src/repository/index.js +14 -2
- package/src/rpc/generated/common_grpc_pb.js +1 -0
- package/src/rpc/generated/common_pb.js +536 -0
- package/src/rpc/generated/errors_grpc_pb.js +1 -0
- package/src/rpc/generated/errors_pb.js +482 -0
- package/src/rpc/generated/knowledge_grpc_pb.js +135 -0
- package/src/rpc/generated/knowledge_pb.js +2168 -0
- package/src/rpc/generated/package.json +3 -0
- package/src/security/process.js +35 -9
- package/src/security/trust-boundary.js +2 -2
- package/src/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +51 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +1 -0
- package/src/ui/boot.js +15 -17
- package/src/ui/cli/activity.js +76 -0
- package/src/ui/cli/compaction.js +15 -0
- package/src/ui/cli/footer.js +39 -0
- package/src/ui/cli/help.js +192 -0
- package/src/ui/cli/plan.js +20 -0
- package/src/ui/cli/runtime-events.js +110 -0
- package/src/ui/cli/waiting.js +16 -0
- package/src/ui/merkle/render.js +1 -1
- package/test/account-session.test.mjs +36 -0
- package/test/cli/preferences-runtime.test.mjs +11 -0
- package/test/cli/runtime-ui.test.mjs +74 -0
- package/test/cli-preferences.test.mjs +26 -5
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +61 -12
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +115 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -0
- package/test/fixtures/knowledge-rpc-worker.mjs +16 -0
- package/test/integration/cli-help.test.mjs +37 -0
- package/test/integration/knowledge-rpc.test.mjs +112 -0
- package/test/integration/merkle-cli.test.mjs +61 -0
- package/test/integration/merkle-persistence-identity.test.mjs +48 -0
- package/test/integration/provider-env-cli.test.mjs +49 -0
- package/test/integration/provider-factory.test.mjs +197 -0
- package/test/integration/repository-company-graph.test.mjs +90 -0
- package/test/integration/runtime-migrations.test.mjs +82 -0
- package/test/knowledge-service.test.mjs +5 -0
- package/test/knowledge.test.mjs +16 -0
- package/test/live/terminal-transport.live.test.mjs +24 -0
- package/test/local-sessions.test.mjs +48 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +127 -16
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +21 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/portable-context.test.mjs +1 -1
- package/test/provider-credentials.test.mjs +96 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +13 -5
- package/test/responses-runner.test.mjs +150 -0
- package/test/shell.test.mjs +92 -23
- package/test/smoke.mjs +4 -1
- package/test/telemetry.test.mjs +79 -0
- package/test/terminal/local-pty.mock.test.mjs +151 -0
- package/test/tools-search.test.mjs +14 -1
- package/test/whoami-resume.test.mjs +56 -0
- /package/{src/lib → packages/agentsam-repository/src}/git-context.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/diff.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/filemeta.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/git-ignore.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/hash.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/semantic.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/snapshot.js +0 -0
- /package/{src/lib → packages/agentsam-repository/src}/merkle/tree.js +0 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import {
|
|
8
|
+
DEPLOY_INPUT_GLOBS,
|
|
9
|
+
loadOptionalCloudflareEnv,
|
|
10
|
+
resolveLocalStudioDeployable,
|
|
11
|
+
wranglerDeployCommand,
|
|
12
|
+
isLocalStudioCheckout,
|
|
13
|
+
runLocalStudioDeploy,
|
|
14
|
+
} from '../src/lib/deploy/local-studio.js';
|
|
15
|
+
import { productionDeployBlockedReason } from '../src/lib/deploy/git-guard.js';
|
|
16
|
+
|
|
17
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
|
|
19
|
+
describe('local-studio deploy resolver', () => {
|
|
20
|
+
it('resolves apps/local-studio + backend/wrangler.jsonc from the repo', () => {
|
|
21
|
+
const target = resolveLocalStudioDeployable(root);
|
|
22
|
+
assert.equal(target.provider, 'cloudflare');
|
|
23
|
+
assert.equal(target.app, 'local-studio');
|
|
24
|
+
assert.equal(path.basename(path.dirname(target.wranglerConfig)), 'backend');
|
|
25
|
+
assert.deepEqual(target.wranglerArgs, ['deploy', '-c', 'backend/wrangler.jsonc']);
|
|
26
|
+
assert.equal(isLocalStudioCheckout(root), true);
|
|
27
|
+
const cmd = wranglerDeployCommand(target, { dryRun: true });
|
|
28
|
+
assert.equal(cmd.bin, 'npx');
|
|
29
|
+
assert.equal(cmd.cwd, target.appRoot);
|
|
30
|
+
assert.deepEqual(cmd.args, ['wrangler', 'deploy', '-c', 'backend/wrangler.jsonc', '--dry-run']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('fingerprints the cloudflare connector package', () => {
|
|
34
|
+
assert.equal(DEPLOY_INPUT_GLOBS.includes('packages/connectors/cloudflare'), true);
|
|
35
|
+
assert.equal(DEPLOY_INPUT_GLOBS.includes('apps/local-studio'), true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('treats missing .env.cloudflare as optional', () => {
|
|
39
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-env-'));
|
|
40
|
+
const loaded = loadOptionalCloudflareEnv(tmp);
|
|
41
|
+
assert.equal(loaded.loaded, false);
|
|
42
|
+
assert.deepEqual(loaded.vars, {});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('plan does not invoke wrangler', async () => {
|
|
46
|
+
const result = await runLocalStudioDeploy({ cwd: root, planOnly: true, execute: false });
|
|
47
|
+
assert.equal(result.plan.provider, 'cloudflare');
|
|
48
|
+
assert.equal(result.plan.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
|
|
49
|
+
assert.equal(result.plan.genericRootDeploy, false);
|
|
50
|
+
assert.equal(result.receipt.plan, true);
|
|
51
|
+
assert.equal(result.receipt.promoted, false);
|
|
52
|
+
assert.equal(result.receipt.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
|
|
53
|
+
assert.equal(result.receipt.hostname, 'agentsam.inneranimalmedia.com');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('blocks production deploys from feat branches', () => {
|
|
57
|
+
const reason = productionDeployBlockedReason({
|
|
58
|
+
ok: true,
|
|
59
|
+
branch: 'feat/cloudflare-connector-deploy-control',
|
|
60
|
+
detached: false,
|
|
61
|
+
head: 'aaa',
|
|
62
|
+
originMain: 'aaa',
|
|
63
|
+
dirty: false,
|
|
64
|
+
tmpCheckout: false,
|
|
65
|
+
repoRoot: root,
|
|
66
|
+
});
|
|
67
|
+
assert.match(reason, /feat\/cloudflare-connector-deploy-control/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('allows production only when HEAD is a clean origin/main', () => {
|
|
71
|
+
const reason = productionDeployBlockedReason({
|
|
72
|
+
ok: true,
|
|
73
|
+
branch: 'main',
|
|
74
|
+
detached: false,
|
|
75
|
+
head: 'abc',
|
|
76
|
+
originMain: 'abc',
|
|
77
|
+
dirty: false,
|
|
78
|
+
tmpCheckout: false,
|
|
79
|
+
repoRoot: root,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(reason, null);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { calculateModelCost, getModelRecord } from '../src/models/index.js';
|
|
4
|
+
|
|
5
|
+
test('Astra catalog separates technical window, reasoning controls, service tiers, and economics', () => {
|
|
6
|
+
const model = getModelRecord('gpt-6-astra');
|
|
7
|
+
assert.ok(model);
|
|
8
|
+
assert.equal(model.context_window, 1_050_000);
|
|
9
|
+
assert.equal(model.max_output_tokens, 128_000);
|
|
10
|
+
assert.deepEqual(model.reasoning_efforts, ['low', 'medium', 'high', 'xhigh', 'max']);
|
|
11
|
+
assert.deepEqual(model.service_tiers, ['default', 'fast', 'flex']);
|
|
12
|
+
assert.equal(model.context_policy.target_input_tokens, 120_000);
|
|
13
|
+
assert.equal(model.context_policy.compact_at_tokens, 180_000);
|
|
14
|
+
assert.equal(model.context_policy.max_normal_input_tokens, 250_000);
|
|
15
|
+
assert.equal(model.context_policy.pricing_threshold_tokens, 272_000);
|
|
16
|
+
assert.equal(model.batch.interactive, false);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('cost calculation applies the long-context threshold to the full request only after crossing it', () => {
|
|
20
|
+
const atBoundary = calculateModelCost('gpt-6-astra', { input_tokens: 272_000, output_tokens: 10_000 });
|
|
21
|
+
assert.equal(atBoundary.threshold_applied, null);
|
|
22
|
+
assert.equal(atBoundary.rates_per_million.input, 10);
|
|
23
|
+
assert.equal(atBoundary.rates_per_million.output, 50);
|
|
24
|
+
|
|
25
|
+
const crossed = calculateModelCost('gpt-6-astra', { input_tokens: 272_001, output_tokens: 10_000 });
|
|
26
|
+
assert.equal(crossed.threshold_applied.input_tokens_gt, 272_000);
|
|
27
|
+
assert.equal(crossed.rates_per_million.input, 20);
|
|
28
|
+
assert.equal(crossed.rates_per_million.output, 75);
|
|
29
|
+
assert.ok(crossed.total_usd > atBoundary.total_usd);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('service tier and cache pricing stay explicit rather than hidden in prompts', () => {
|
|
33
|
+
const usage = { input_tokens: 100_000, cached_input_tokens: 80_000, cache_write_tokens: 10_000, output_tokens: 20_000 };
|
|
34
|
+
const standard = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'default' });
|
|
35
|
+
const fast = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'fast' });
|
|
36
|
+
const flex = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'flex' });
|
|
37
|
+
const batch = calculateModelCost('gpt-6-astra', usage, { serviceTier: 'batch' });
|
|
38
|
+
assert.equal(fast.total_usd, standard.total_usd * 2);
|
|
39
|
+
assert.equal(flex.total_usd, standard.total_usd * 0.5);
|
|
40
|
+
assert.equal(batch.total_usd, standard.total_usd * 0.5);
|
|
41
|
+
assert.equal(standard.rates_per_million.cached_input, 1);
|
|
42
|
+
assert.equal(standard.rates_per_million.cache_write, 12.5);
|
|
43
|
+
});
|
package/test/models.test.mjs
CHANGED
|
@@ -1,37 +1,148 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
2
5
|
import test from 'node:test';
|
|
3
|
-
|
|
4
6
|
import { collectModelsStatus, renderModelsStatus } from '../src/commands/models.js';
|
|
5
7
|
|
|
6
8
|
function response(body, status = 200) {
|
|
7
|
-
return {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
return { ok: status >= 200 && status < 300, status, async json() { return body; } };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function tempHome(t) {
|
|
13
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-model-home-'));
|
|
14
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
15
|
+
return home;
|
|
12
16
|
}
|
|
13
17
|
|
|
14
|
-
test('model inventory reports configured API providers without exposing credentials', async
|
|
18
|
+
test('model inventory reports configured API providers without exposing credentials', async t => {
|
|
19
|
+
const home = tempHome(t);
|
|
15
20
|
const status = await collectModelsStatus({
|
|
21
|
+
home,
|
|
22
|
+
discoverRemote: false,
|
|
16
23
|
env: {
|
|
17
|
-
OPENAI_API_KEY: 'secret-openai',
|
|
18
|
-
|
|
19
|
-
XAI_API_KEY: 'secret-xai',
|
|
20
|
-
OLLAMA_BASE_URL: 'http://127.0.0.1:11434',
|
|
21
|
-
OLLAMA_MODEL: 'qwen:test',
|
|
22
|
-
OLLAMA_EMBED_MODEL: 'embed:test',
|
|
24
|
+
OPENAI_API_KEY: 'secret-openai', GEMINI_API_KEY: '', XAI_API_KEY: 'secret-xai', ANTHROPIC_API_KEY: 'secret-anthropic', CLOUDFLARE_API_TOKEN: 'secret-cf', ACCOUNT_ID: '33333333333333333333333333333333',
|
|
25
|
+
OLLAMA_BASE_URL: 'http://127.0.0.1:11434', OLLAMA_MODEL: 'qwen:test', OLLAMA_EMBED_MODEL: 'embed:test',
|
|
23
26
|
},
|
|
24
27
|
fetchImpl: async () => response({ models: [{ name: 'qwen:test' }, { name: 'embed:test' }] }),
|
|
25
28
|
});
|
|
26
|
-
|
|
27
29
|
assert.equal(status.providers.find((row) => row.id === 'openai').configured, true);
|
|
28
30
|
assert.equal(status.providers.find((row) => row.id === 'gemini').configured, false);
|
|
29
31
|
assert.equal(status.providers.find((row) => row.id === 'grok').configured, true);
|
|
32
|
+
assert.equal(status.providers.find((row) => row.id === 'anthropic').configured, true);
|
|
33
|
+
assert.equal(status.providers.find((row) => row.id === 'cloudflare').configured, true);
|
|
30
34
|
assert.equal(status.local.online, true);
|
|
31
35
|
assert.deepEqual(status.local.models.map((row) => row.name), ['qwen:test', 'embed:test']);
|
|
32
|
-
|
|
33
36
|
const rendered = renderModelsStatus(status);
|
|
34
37
|
assert.match(rendered, /OpenAI/);
|
|
35
38
|
assert.match(rendered, /qwen:test/);
|
|
36
|
-
assert.doesNotMatch(rendered, /secret-openai|secret-xai/);
|
|
39
|
+
assert.doesNotMatch(rendered, /secret-openai|secret-xai|secret-anthropic|secret-cf/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('static/reference metadata never invents hosted model availability', async () => {
|
|
43
|
+
const status = await collectModelsStatus({
|
|
44
|
+
env: { OPENAI_API_KEY: 'secret-openai', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
|
|
45
|
+
fetchImpl: async () => response({ models: [] }),
|
|
46
|
+
providerFetchImpl: async () => response({ data: [{ id: 'some-other-model' }] }),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
assert.deepEqual(status.availableModels.map((row) => row.provider_model_id), ['some-other-model']);
|
|
50
|
+
const referenceOnly = status.catalogModels.find((row) => row.provider_model_id === 'gpt-6-astra');
|
|
51
|
+
assert.equal(referenceOnly?.availability, 'unverified');
|
|
52
|
+
assert.equal(referenceOnly?.availability_source, 'sdk_reference');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('exact hosted model availability is verified against the provider inventory', async () => {
|
|
56
|
+
const status = await collectModelsStatus({
|
|
57
|
+
env: { OPENAI_API_KEY: 'secret-openai', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
|
|
58
|
+
fetchImpl: async () => response({ models: [] }),
|
|
59
|
+
providerFetchImpl: async () => response({ data: [{ id: 'gpt-6-astra' }, { id: 'some-other-model' }] }),
|
|
60
|
+
});
|
|
61
|
+
assert.equal(status.discovery.openai.ok, true);
|
|
62
|
+
assert.equal(status.discovery.openai.returnedModelCount, 2);
|
|
63
|
+
assert.deepEqual(status.availableModels.map((row) => row.provider_model_id), ['gpt-6-astra', 'some-other-model']);
|
|
64
|
+
assert.equal(status.availableModels[0].availability_source, 'provider_api');
|
|
65
|
+
assert.equal(status.availableModels[0].context_window_source, 'sdk_reference');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
test('Gemini and xAI discovery keep per-key limits from provider metadata', async () => {
|
|
72
|
+
const seen = [];
|
|
73
|
+
const status = await collectModelsStatus({
|
|
74
|
+
env: {
|
|
75
|
+
GEMINI_API_KEY: 'gem-key',
|
|
76
|
+
XAI_API_KEY: 'xai-key',
|
|
77
|
+
OLLAMA_BASE_URL: 'http://127.0.0.1:11434',
|
|
78
|
+
},
|
|
79
|
+
fetchImpl: async () => response({ models: [] }),
|
|
80
|
+
providerFetchImpl: async (url, options) => {
|
|
81
|
+
seen.push(url);
|
|
82
|
+
if (url.includes('generativelanguage.googleapis.com')) {
|
|
83
|
+
return response({ models: [{
|
|
84
|
+
name: 'models/gemini-test',
|
|
85
|
+
baseModelId: 'gemini-test',
|
|
86
|
+
displayName: 'Gemini Test',
|
|
87
|
+
inputTokenLimit: 123456,
|
|
88
|
+
outputTokenLimit: 8192,
|
|
89
|
+
supportedGenerationMethods: ['generateContent'],
|
|
90
|
+
thinking: true,
|
|
91
|
+
}] });
|
|
92
|
+
}
|
|
93
|
+
if (url.includes('api.x.ai')) {
|
|
94
|
+
return response({ data: [{
|
|
95
|
+
id: 'grok-test',
|
|
96
|
+
context_length: 256000,
|
|
97
|
+
prompt_text_token_price: 1000,
|
|
98
|
+
cached_prompt_text_token_price: 500,
|
|
99
|
+
completion_text_token_price: 4000,
|
|
100
|
+
}] });
|
|
101
|
+
}
|
|
102
|
+
throw new Error('unexpected URL ' + url);
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const gemini = status.providerModels.gemini[0];
|
|
107
|
+
assert.equal(gemini.provider_model_id, 'gemini-test');
|
|
108
|
+
assert.equal(gemini.context_window, 123456);
|
|
109
|
+
assert.equal(gemini.context_window_source, 'provider_api');
|
|
110
|
+
assert.deepEqual(gemini.reasoning_efforts, ['auto', 'low', 'medium', 'high']);
|
|
111
|
+
|
|
112
|
+
const grok = status.providerModels.grok[0];
|
|
113
|
+
assert.equal(grok.provider_model_id, 'grok-test');
|
|
114
|
+
assert.equal(grok.context_window, 256000);
|
|
115
|
+
assert.equal(grok.pricing.input, 1);
|
|
116
|
+
assert.equal(grok.pricing.output, 4);
|
|
117
|
+
assert.equal(seen.some((url) => url.includes('generativelanguage.googleapis.com')), true);
|
|
118
|
+
assert.equal(seen.some((url) => url.includes('api.x.ai')), true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
test('Cloudflare discovery is scoped to the loaded account and surfaces text-generation models only', async t => {
|
|
123
|
+
const seen = [];
|
|
124
|
+
const home = tempHome(t);
|
|
125
|
+
const status = await collectModelsStatus({
|
|
126
|
+
home,
|
|
127
|
+
env: { CLOUDFLARE_API_TOKEN: 'secret-cf', ACCOUNT_ID: '44444444444444444444444444444444', OLLAMA_BASE_URL: 'http://127.0.0.1:11434' },
|
|
128
|
+
fetchImpl: async () => response({ models: [] }),
|
|
129
|
+
providerFetchImpl: async (url, options) => {
|
|
130
|
+
seen.push({ url, auth: options.headers.authorization });
|
|
131
|
+
return response({ success: true, result: [
|
|
132
|
+
{ name: '@cf/qwen/code', task: { name: 'Text Generation' }, description: 'coding' },
|
|
133
|
+
{ name: '@cf/baai/embed', task: { name: 'Text Embeddings' }, description: 'embeddings' },
|
|
134
|
+
] });
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
assert.equal(seen.length, 1);
|
|
138
|
+
assert.match(seen[0].url, /accounts\/44444444444444444444444444444444\/ai\/models\/search$/);
|
|
139
|
+
assert.equal(status.discovery.cloudflare.ok, true);
|
|
140
|
+
assert.equal(status.discovery.cloudflare.returnedModelCount, 1);
|
|
141
|
+
assert.deepEqual(status.providerModels.cloudflare.map((row) => row.provider_model_id), ['@cf/qwen/code']);
|
|
142
|
+
assert.equal(status.providerModels.cloudflare[0].availability_source, 'provider_api');
|
|
143
|
+
assert.equal(status.providerModels.cloudflare[0].context_window_source, 'unknown');
|
|
144
|
+
const rendered = renderModelsStatus(status);
|
|
145
|
+
assert.match(rendered, /@cf\/qwen\/code/);
|
|
146
|
+
assert.doesNotMatch(rendered, /@cf\/baai\/embed/);
|
|
147
|
+
assert.doesNotMatch(rendered, /secret-cf/);
|
|
37
148
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { describe, it } from 'node:test';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const lockPath = path.join(root, 'apps/local-studio/package-lock.json');
|
|
10
|
+
|
|
11
|
+
describe('local-studio npm 10 lockfile', () => {
|
|
12
|
+
it('pins lru-cache@11.5.2 so Cloudflare npm ci can resolve it', () => {
|
|
13
|
+
const lock = JSON.parse(fs.readFileSync(lockPath, 'utf8'));
|
|
14
|
+
assert.equal(lock.lockfileVersion, 3);
|
|
15
|
+
const pkgs = lock.packages || {};
|
|
16
|
+
const hits = Object.entries(pkgs).filter(([key, meta]) => key.includes('lru-cache') && meta.version === '11.5.2');
|
|
17
|
+
assert.ok(hits.length >= 1, 'Missing: lru-cache@11.5.2 from lock file');
|
|
18
|
+
const [, meta] = hits[0];
|
|
19
|
+
assert.match(meta.resolved || '', /lru-cache-11\.5\.2\.tgz/);
|
|
20
|
+
assert.ok(meta.integrity);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('verify-npm10-lock script exits 0 against the current lock', () => {
|
|
24
|
+
const script = path.join(root, 'apps/local-studio/scripts/verify-npm10-lock.mjs');
|
|
25
|
+
const r = spawnSync(process.execPath, [script], { encoding: 'utf8' });
|
|
26
|
+
assert.equal(r.status, 0, r.stderr || r.stdout);
|
|
27
|
+
assert.match(r.stdout, /lru-cache@11\.5\.2/);
|
|
28
|
+
});
|
|
29
|
+
});
|
package/test/ollama.test.mjs
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
ollamaInstallPlan,
|
|
9
9
|
parseOllamaArgs,
|
|
10
10
|
probeOllama,
|
|
11
|
+
probeOllamaModel,
|
|
11
12
|
resolveOllamaConfig,
|
|
12
13
|
updateProjectOllamaConfig,
|
|
13
14
|
upsertOllamaEnvFile,
|
|
@@ -82,6 +83,26 @@ test('status probe recognizes configured chat and embedding models', async () =>
|
|
|
82
83
|
assert.equal(status.embed_ready, true);
|
|
83
84
|
});
|
|
84
85
|
|
|
86
|
+
test('local model metadata is accepted only after Ollama itself verifies the model', async () => {
|
|
87
|
+
let seenBody = null;
|
|
88
|
+
const fakeFetch = async (url, init) => {
|
|
89
|
+
assert.match(url, /\/api\/show$/);
|
|
90
|
+
seenBody = JSON.parse(init.body);
|
|
91
|
+
return new Response(JSON.stringify({
|
|
92
|
+
model_info: { 'qwen2.context_length': 32768 },
|
|
93
|
+
capabilities: ['completion', 'tools'],
|
|
94
|
+
details: { family: 'qwen2' },
|
|
95
|
+
}), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await probeOllamaModel('qwen2.5-coder', OLLAMA_DEFAULTS, fakeFetch);
|
|
99
|
+
assert.equal(seenBody.model, 'qwen2.5-coder');
|
|
100
|
+
assert.equal(result.ok, true);
|
|
101
|
+
assert.equal(result.context_window, 32768);
|
|
102
|
+
assert.equal(result.context_window_source, 'local_runtime');
|
|
103
|
+
assert.deepEqual(result.capabilities, ['completion', 'tools']);
|
|
104
|
+
});
|
|
105
|
+
|
|
85
106
|
test('automatic install plans use local package managers, never a remote shell script', () => {
|
|
86
107
|
assert.deepEqual(ollamaInstallPlan('darwin', { brew: true }), {
|
|
87
108
|
command: 'brew', args: ['install', 'ollama'], manager: 'homebrew',
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createOpenAIResponsesAdapter } from '../src/providers/index.js';
|
|
4
|
+
|
|
5
|
+
function jsonResponse(body, status = 200) {
|
|
6
|
+
return { ok: status >= 200 && status < 300, status, async json() { return body; } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
test('Responses adapter sends reasoning/service tier without legacy sampling parameters and reconciles provider usage', async () => {
|
|
10
|
+
let request = null;
|
|
11
|
+
const events = [];
|
|
12
|
+
const adapter = createOpenAIResponsesAdapter({
|
|
13
|
+
apiKey: 'test-key',
|
|
14
|
+
emit: event => events.push(event),
|
|
15
|
+
fetchImpl: async (url, options) => {
|
|
16
|
+
request = { url, options, body: JSON.parse(options.body) };
|
|
17
|
+
return jsonResponse({
|
|
18
|
+
id: 'resp_test', status: 'completed', model: 'gpt-6-astra', service_tier: 'fast',
|
|
19
|
+
output: [{ type: 'message', content: [{ type: 'output_text', text: 'done' }] }],
|
|
20
|
+
usage: {
|
|
21
|
+
input_tokens: 100_000,
|
|
22
|
+
input_tokens_details: { cached_tokens: 80_000, cache_write_tokens: 10_000 },
|
|
23
|
+
output_tokens: 5_000,
|
|
24
|
+
output_tokens_details: { reasoning_tokens: 2_000 },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const result = await adapter.create({ model: 'gpt-6-astra', input: 'work', reasoningEffort: 'high', serviceTier: 'fast' });
|
|
30
|
+
assert.equal(request.url, 'https://api.openai.com/v1/responses');
|
|
31
|
+
assert.equal(request.body.reasoning.effort, 'high');
|
|
32
|
+
assert.equal(request.body.service_tier, 'fast');
|
|
33
|
+
assert.equal(request.body.truncation, 'disabled');
|
|
34
|
+
assert.equal('temperature' in request.body, false);
|
|
35
|
+
assert.equal('top_p' in request.body, false);
|
|
36
|
+
assert.equal(result.output_text, 'done');
|
|
37
|
+
assert.equal(result.usage_snapshot.current_context.input_tokens, 100_000);
|
|
38
|
+
assert.equal(result.usage_snapshot.current_context.window_tokens, 1_050_000);
|
|
39
|
+
assert.equal(result.actual_service_tier, 'fast');
|
|
40
|
+
assert.equal(result.cost.rates_per_million.input, 20);
|
|
41
|
+
assert.deepEqual(events.map(event => event.type), ['model.started', 'usage.snapshot', 'cost.snapshot', 'model.completed']);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Responses adapter preserves function call_id when returning tool output', async () => {
|
|
45
|
+
const bodies = [];
|
|
46
|
+
const adapter = createOpenAIResponsesAdapter({
|
|
47
|
+
apiKey: 'test-key',
|
|
48
|
+
fetchImpl: async (_url, options) => {
|
|
49
|
+
const body = JSON.parse(options.body);
|
|
50
|
+
bodies.push(body);
|
|
51
|
+
if (bodies.length === 1) {
|
|
52
|
+
return jsonResponse({
|
|
53
|
+
id: 'resp_calls', status: 'completed', service_tier: 'default',
|
|
54
|
+
output: [{ type: 'function_call', id: 'fc_1', call_id: 'call_abc', name: 'repository_snapshot', arguments: '{"depth":"quick"}', status: 'completed' }],
|
|
55
|
+
usage: { input_tokens: 100, output_tokens: 20 },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return jsonResponse({
|
|
59
|
+
id: 'resp_done', status: 'completed', service_tier: 'default',
|
|
60
|
+
output: [{ type: 'message', content: [{ type: 'output_text', text: 'complete' }] }],
|
|
61
|
+
usage: { input_tokens: 200, output_tokens: 30 },
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
const first = await adapter.create({
|
|
66
|
+
model: 'gpt-6-astra', input: 'inspect', reasoningEffort: 'low', serviceTier: 'default',
|
|
67
|
+
tools: [{ type: 'function', name: 'repository_snapshot', description: 'Snapshot repo', parameters: { type: 'object', properties: {} } }],
|
|
68
|
+
});
|
|
69
|
+
assert.equal(first.tool_calls[0].call_id, 'call_abc');
|
|
70
|
+
const second = await adapter.continueWithToolOutputs({
|
|
71
|
+
model: 'gpt-6-astra', previousResponseId: first.response_id, reasoningEffort: 'low', serviceTier: 'default',
|
|
72
|
+
toolOutputs: [{ call_id: first.tool_calls[0].call_id, output: { ok: true } }],
|
|
73
|
+
});
|
|
74
|
+
assert.equal(bodies[1].previous_response_id, 'resp_calls');
|
|
75
|
+
assert.deepEqual(bodies[1].input, [{ type: 'function_call_output', call_id: 'call_abc', output: '{"ok":true}' }]);
|
|
76
|
+
assert.equal(second.output_text, 'complete');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('compaction is an explicit provider operation and has no arbitrary default wall-clock timeout', async () => {
|
|
80
|
+
let signal = 'unset';
|
|
81
|
+
let body = null;
|
|
82
|
+
const adapter = createOpenAIResponsesAdapter({
|
|
83
|
+
apiKey: 'test-key',
|
|
84
|
+
fetchImpl: async (url, options) => {
|
|
85
|
+
assert.equal(url, 'https://api.openai.com/v1/responses/compact');
|
|
86
|
+
signal = options.signal;
|
|
87
|
+
body = JSON.parse(options.body);
|
|
88
|
+
return jsonResponse({ id: 'cmp_1', object: 'response.compaction', output: [{ type: 'compaction', encrypted_content: 'opaque' }], usage: { input_tokens: 1_000, output_tokens: 100 } });
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
const compacted = await adapter.compact({ model: 'gpt-6-astra', previousResponseId: 'resp_previous' });
|
|
92
|
+
assert.equal(signal, undefined);
|
|
93
|
+
assert.equal(body.previous_response_id, 'resp_previous');
|
|
94
|
+
assert.equal(compacted.compaction_id, 'cmp_1');
|
|
95
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { describeProviderCredential, ensureProviderEnvProfile, resolveProviderCredential } from '../src/lib/provider-credentials.js';
|
|
7
|
+
|
|
8
|
+
function fixtureHome(t) {
|
|
9
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-credentials-'));
|
|
10
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
11
|
+
fs.mkdirSync(path.join(home, '.agentsam', 'env.d'), { recursive: true });
|
|
12
|
+
return home;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('provider credential resolver loads a secure AgentSam env file without exposing it in status', t => {
|
|
16
|
+
const home = fixtureHome(t);
|
|
17
|
+
const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
|
|
18
|
+
fs.writeFileSync(filename, 'export OPENAI_API_KEY="secret-from-file"\n', { mode: 0o600 });
|
|
19
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
20
|
+
|
|
21
|
+
const resolved = resolveProviderCredential('openai', { env: {}, home });
|
|
22
|
+
assert.equal(resolved.configured, true);
|
|
23
|
+
assert.equal(resolved.source, 'agentsam_env_file');
|
|
24
|
+
assert.equal(resolved.value, 'secret-from-file');
|
|
25
|
+
|
|
26
|
+
const safe = describeProviderCredential('openai', { env: {}, home });
|
|
27
|
+
assert.equal(safe.configured, true);
|
|
28
|
+
assert.equal(safe.source, 'agentsam_env_file');
|
|
29
|
+
assert.equal(Object.hasOwn(safe, 'value'), false);
|
|
30
|
+
assert.doesNotMatch(JSON.stringify(safe), /secret-from-file/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('environment credential wins over disk fallback', t => {
|
|
34
|
+
const home = fixtureHome(t);
|
|
35
|
+
const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
|
|
36
|
+
fs.writeFileSync(filename, 'OPENAI_API_KEY=file-value\n', { mode: 0o600 });
|
|
37
|
+
if (process.platform !== 'win32') fs.chmodSync(filename, 0o600);
|
|
38
|
+
const resolved = resolveProviderCredential('openai', { env: { OPENAI_API_KEY: 'env-value' }, home });
|
|
39
|
+
assert.equal(resolved.source, 'environment');
|
|
40
|
+
assert.equal(resolved.value, 'env-value');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('AgentSam refuses provider credential files with broad POSIX permissions', { skip: process.platform === 'win32' }, t => {
|
|
44
|
+
const home = fixtureHome(t);
|
|
45
|
+
const filename = path.join(home, '.agentsam', 'env.d', 'openai.env');
|
|
46
|
+
fs.writeFileSync(filename, 'OPENAI_API_KEY=unsafe-value\n', { mode: 0o644 });
|
|
47
|
+
fs.chmodSync(filename, 0o644);
|
|
48
|
+
const resolved = resolveProviderCredential('openai', { env: {}, home });
|
|
49
|
+
assert.equal(resolved.configured, false);
|
|
50
|
+
assert.equal(resolved.error, 'permissions_too_open');
|
|
51
|
+
assert.equal(resolved.value, '');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
test('provider env profiles create a secure reusable source loader without embedding secrets', { skip: process.platform === 'win32' }, t => {
|
|
56
|
+
const home = fixtureHome(t);
|
|
57
|
+
const openai = ensureProviderEnvProfile('openai', { home });
|
|
58
|
+
assert.equal(openai.source_command, 'source ~/.agentsam/load-agent-env.sh openai');
|
|
59
|
+
assert.equal(fs.statSync(openai.file).mode & 0o777, 0o600);
|
|
60
|
+
assert.equal(fs.statSync(openai.loader).mode & 0o777, 0o700);
|
|
61
|
+
assert.match(fs.readFileSync(openai.file, 'utf8'), /OPENAI_API_KEY=""/);
|
|
62
|
+
assert.doesNotMatch(fs.readFileSync(openai.loader, 'utf8'), /sk-|secret-/);
|
|
63
|
+
|
|
64
|
+
const cloudflare = ensureProviderEnvProfile('cloudflare', { home });
|
|
65
|
+
const source = fs.readFileSync(cloudflare.file, 'utf8');
|
|
66
|
+
assert.match(source, /ACCOUNT_ID=""/);
|
|
67
|
+
assert.match(source, /CLOUDFLARE_API_TOKEN=""/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('Cloudflare credential status carries non-secret account identity from the provider profile', t => {
|
|
71
|
+
const home = fixtureHome(t);
|
|
72
|
+
const profile = path.join(home, '.agentsam', 'env.d', 'cloudflare.env');
|
|
73
|
+
fs.writeFileSync(profile, 'export ACCOUNT_ID="0123456789abcdef0123456789abcdef"\nexport CLOUDFLARE_API_TOKEN="token-secret"\n', { mode: 0o600 });
|
|
74
|
+
if (process.platform !== 'win32') fs.chmodSync(profile, 0o600);
|
|
75
|
+
const resolved = resolveProviderCredential('cloudflare', { env: {}, home });
|
|
76
|
+
assert.equal(resolved.account_id, '0123456789abcdef0123456789abcdef');
|
|
77
|
+
const safe = describeProviderCredential('cloudflare', { env: {}, home });
|
|
78
|
+
assert.equal(safe.account_id, '0123456789abcdef0123456789abcdef');
|
|
79
|
+
assert.doesNotMatch(JSON.stringify(safe), /token-secret/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
test('Cloudflare profile account backfill never overwrites an existing token or account choice', t => {
|
|
84
|
+
const home = fixtureHome(t);
|
|
85
|
+
const profile = path.join(home, '.agentsam', 'env.d', 'cloudflare.env');
|
|
86
|
+
fs.writeFileSync(profile, 'export ACCOUNT_ID=""\nexport CLOUDFLARE_API_TOKEN="keep-me"\n', { mode: 0o600 });
|
|
87
|
+
ensureProviderEnvProfile('cloudflare', { home, accountId: '11111111111111111111111111111111' });
|
|
88
|
+
let source = fs.readFileSync(profile, 'utf8');
|
|
89
|
+
assert.match(source, /ACCOUNT_ID="11111111111111111111111111111111"/);
|
|
90
|
+
assert.match(source, /CLOUDFLARE_API_TOKEN="keep-me"/);
|
|
91
|
+
|
|
92
|
+
ensureProviderEnvProfile('cloudflare', { home, accountId: '22222222222222222222222222222222' });
|
|
93
|
+
source = fs.readFileSync(profile, 'utf8');
|
|
94
|
+
assert.match(source, /ACCOUNT_ID="11111111111111111111111111111111"/);
|
|
95
|
+
assert.doesNotMatch(source, /22222222222222222222222222222222/);
|
|
96
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import test from 'node:test';
|
|
4
|
+
import { compactConsumedToolResult, rehydrateContextRef } from '../src/context/index.js';
|
|
5
|
+
|
|
6
|
+
const sha = value => `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
7
|
+
|
|
8
|
+
test('large consumed evidence can leave active context and later rehydrate by stable ref/hash', async () => {
|
|
9
|
+
const source = 'important evidence '.repeat(800);
|
|
10
|
+
const ref = 'tool:call_123';
|
|
11
|
+
const digest = sha(source);
|
|
12
|
+
const compacted = compactConsumedToolResult(source, { ref, hash: digest });
|
|
13
|
+
assert.equal(compacted.compacted, true);
|
|
14
|
+
assert.equal(compacted.chars, 4_000);
|
|
15
|
+
assert.equal(compacted.source_chars, source.length);
|
|
16
|
+
const adapter = { async read(requested) { assert.equal(requested, ref); return { content: source, hash: digest, kind: 'tool_result' }; } };
|
|
17
|
+
const restored = await rehydrateContextRef(ref, adapter, { expectedHash: digest, kind: 'tool_result', maxChars: 64_000 });
|
|
18
|
+
assert.equal(restored.content, source);
|
|
19
|
+
assert.equal(restored.hash, digest);
|
|
20
|
+
assert.equal(restored.truncated, false);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('rehydration fails closed when stable source hash changed', async () => {
|
|
24
|
+
await assert.rejects(() => rehydrateContextRef('file:x', async () => ({ content: 'changed' }), { expectedHash: sha('original') }), /rehydration_hash_mismatch/);
|
|
25
|
+
});
|
|
@@ -6,15 +6,23 @@ import test from 'node:test';
|
|
|
6
6
|
const root = path.resolve(import.meta.dirname, '..');
|
|
7
7
|
const read = (rel) => fs.readFileSync(path.join(root, rel), 'utf8');
|
|
8
8
|
|
|
9
|
-
test('
|
|
9
|
+
test('release candidate metadata and public terminal vocabulary are aligned', () => {
|
|
10
10
|
const pkg = JSON.parse(read('package.json'));
|
|
11
11
|
const manifest = read('agentsam.yaml');
|
|
12
12
|
const readme = read('README.md');
|
|
13
|
+
const escapedVersion = pkg.version.replaceAll('.', '\\.');
|
|
13
14
|
|
|
14
|
-
assert.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
assert.match(
|
|
16
|
+
manifest,
|
|
17
|
+
new RegExp(`packages:[\\s\\S]*?root_sdk:[\\s\\S]*?version: "${escapedVersion}"`),
|
|
18
|
+
);
|
|
19
|
+
assert.match(
|
|
20
|
+
manifest,
|
|
21
|
+
new RegExp(`release:[\\s\\S]*?root_sdk:[\\s\\S]*?target_version: "${escapedVersion}"`),
|
|
22
|
+
);
|
|
23
|
+
assert.match(manifest, /state: (candidate|published)/);
|
|
24
|
+
assert.match(manifest, /current_latest: "\d+\.\d+\.\d+"/);
|
|
25
|
+
assert.match(manifest, /verification_command: npm run verify:release/);
|
|
18
26
|
assert.doesNotMatch(manifest, /^\s*- tui\s*$/m);
|
|
19
27
|
assert.doesNotMatch(readme, /agentsam tui|CLI\/TUI/);
|
|
20
28
|
});
|