@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,75 @@
|
|
|
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 { buildWranglerInvocation, listWranglerNativeCommands, runWranglerNative, summarizeCloudflareCpuProfile, summarizeCloudflareCpuProfileFile } from '../src/cloudflare/index.js';
|
|
7
|
+
import { createCapabilityAdapter } from '../src/agent/capability-adapter.js';
|
|
8
|
+
|
|
9
|
+
function tempRoot() { return fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-cf-')); }
|
|
10
|
+
|
|
11
|
+
test('Cloudflare native command catalog exposes bounded read operations and never an auth-token secret read', () => {
|
|
12
|
+
const ids = listWranglerNativeCommands().map((row) => row.id);
|
|
13
|
+
assert.deepEqual(ids, ['whoami', 'deployments.list', 'versions.list', 'types.check', 'queues.list']);
|
|
14
|
+
assert.ok(!ids.some((id) => id.includes('token') || id.includes('secret') || id === 'deploy'));
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test('Wrangler native invocation is argv-based and cwd/config scoped', () => {
|
|
18
|
+
const root = tempRoot();
|
|
19
|
+
fs.writeFileSync(path.join(root, 'wrangler.jsonc'), '{}');
|
|
20
|
+
const plan = buildWranglerInvocation('deployments.list', { cwd: root, name: 'demo' });
|
|
21
|
+
assert.equal(plan.cwd, root);
|
|
22
|
+
assert.deepEqual(plan.args.slice(0, 4), ['deployments', 'list', '--json', '--name']);
|
|
23
|
+
assert.equal(plan.args[4], 'demo');
|
|
24
|
+
assert.ok(plan.args.includes('--config'));
|
|
25
|
+
assert.throws(() => buildWranglerInvocation('whoami', { cwd: root, config: '../outside.toml' }), /outside_cwd/);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('Wrangler native result preserves JSON and real exit code on failure', async () => {
|
|
29
|
+
const root = tempRoot();
|
|
30
|
+
const success = await runWranglerNative('whoami', { cwd: root }, { run: async () => ({ code: 0, stdout: JSON.stringify({ email: 'dev@example.com' }), stderr: '' }) });
|
|
31
|
+
assert.equal(success.data.email, 'dev@example.com');
|
|
32
|
+
await assert.rejects(
|
|
33
|
+
runWranglerNative('versions.list', { cwd: root }, { run: async () => ({ code: 7, stdout: '', stderr: 'upstream failed' }) }),
|
|
34
|
+
(error) => error.diagnostic?.exit_code === 7 && error.diagnostic?.code === 'wrangler_exit_nonzero',
|
|
35
|
+
);
|
|
36
|
+
await assert.rejects(
|
|
37
|
+
runWranglerNative('deployments.list', { cwd: root }, { run: async () => ({ code: 1, stdout: '', stderr: 'Authentication error [code: 10000]\nRequest ID: req_cf_1\nCF-Ray: ray-123' }) }),
|
|
38
|
+
(error) => error.diagnostic?.code === '10000' && error.diagnostic?.request_id === 'req_cf_1' && error.diagnostic?.ray_id === 'ray-123',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test('CPU profile summary ranks self-time hotspots and explicitly rejects production timer inference', () => {
|
|
43
|
+
const profile = {
|
|
44
|
+
nodes: [
|
|
45
|
+
{ id: 1, callFrame: { functionName: 'fetch', url: 'worker.js', lineNumber: 0, columnNumber: 0 } },
|
|
46
|
+
{ id: 2, callFrame: { functionName: 'heavyLoop', url: 'worker.js', lineNumber: 10, columnNumber: 2 } },
|
|
47
|
+
{ id: 3, callFrame: { functionName: '(garbage collector)', url: '', lineNumber: -1, columnNumber: -1 } },
|
|
48
|
+
],
|
|
49
|
+
samples: [1, 2, 2, 3],
|
|
50
|
+
timeDeltas: [100, 1200, 800, 400],
|
|
51
|
+
};
|
|
52
|
+
const summary = summarizeCloudflareCpuProfile(profile);
|
|
53
|
+
assert.equal(summary.top_frames[0].function, 'heavyLoop');
|
|
54
|
+
assert.equal(summary.top_frames[0].self_us, 2000);
|
|
55
|
+
assert.equal(summary.garbage_collection_ms, 0.4);
|
|
56
|
+
assert.match(summary.timer_semantics, /do not advance during CPU-only execution/);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('CPU profile file cannot escape runtime cwd', () => {
|
|
60
|
+
const root = tempRoot();
|
|
61
|
+
fs.writeFileSync(path.join(root, 'profile.cpuprofile'), JSON.stringify({ nodes: [], samples: [], timeDeltas: [] }));
|
|
62
|
+
assert.equal(summarizeCloudflareCpuProfileFile({ cwd: root, file: 'profile.cpuprofile' }).file, 'profile.cpuprofile');
|
|
63
|
+
assert.throws(() => summarizeCloudflareCpuProfileFile({ cwd: root, file: '../profile.cpuprofile' }), /outside_cwd/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('explicitly executable experimental Cloudflare capabilities enter cards-first discovery without exposing unavailable commands', () => {
|
|
67
|
+
const adapter = createCapabilityAdapter();
|
|
68
|
+
const rows = adapter.toolDescriptors();
|
|
69
|
+
const ids = rows.map((row) => row.name);
|
|
70
|
+
assert.ok(ids.includes('cloudflare.wrangler.native'));
|
|
71
|
+
assert.ok(ids.includes('cloudflare.cpu.profile'));
|
|
72
|
+
assert.ok(!ids.includes('cloudflare.cpu.audit'));
|
|
73
|
+
assert.ok(!ids.includes('blender.build'));
|
|
74
|
+
assert.equal(rows.find((row) => row.name === 'cloudflare.wrangler.native').risk, 'read');
|
|
75
|
+
});
|
package/test/context.test.mjs
CHANGED
|
@@ -5,6 +5,8 @@ import path from 'node:path';
|
|
|
5
5
|
import test from 'node:test';
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_RESULT_POLICY,
|
|
8
|
+
assessContextUsage,
|
|
9
|
+
compileAgentInstructions,
|
|
8
10
|
createContextBudget,
|
|
9
11
|
normalizeResultPolicy,
|
|
10
12
|
resolveContext,
|
|
@@ -13,18 +15,14 @@ import {
|
|
|
13
15
|
|
|
14
16
|
test('default result policy is bounded and callers may request less', () => {
|
|
15
17
|
assert.deepEqual(DEFAULT_RESULT_POLICY, { max_items: 8, max_chars: 24_000, detail: 'excerpt' });
|
|
16
|
-
assert.deepEqual(normalizeResultPolicy({ max_items: 3, max_chars: 8_000, detail: 'card' }), {
|
|
17
|
-
max_items: 3,
|
|
18
|
-
max_chars: 8_000,
|
|
19
|
-
detail: 'card',
|
|
20
|
-
});
|
|
18
|
+
assert.deepEqual(normalizeResultPolicy({ max_items: 3, max_chars: 8_000, detail: 'card' }), { max_items: 3, max_chars: 8_000, detail: 'card' });
|
|
21
19
|
assert.throws(() => normalizeResultPolicy({ max_items: 9 }), /higher_detail_required:max_items/);
|
|
22
20
|
assert.throws(() => normalizeResultPolicy({ max_chars: 24_001 }), /higher_detail_required:max_chars/);
|
|
23
21
|
assert.throws(() => normalizeResultPolicy({ detail: 'full' }), /higher_detail_required:detail/);
|
|
24
22
|
assert.equal(normalizeResultPolicy({ detail: 'full', max_items: 20, max_chars: 100_000 }, { operation: 'higher-detail' }).detail, 'full');
|
|
25
23
|
});
|
|
26
24
|
|
|
27
|
-
test('context budget
|
|
25
|
+
test('legacy ratio context budget remains compatible for ordinary windows', () => {
|
|
28
26
|
const budget = createContextBudget({ windowTokens: 250_000 });
|
|
29
27
|
assert.equal(budget.targetInputTokens, 150_000);
|
|
30
28
|
assert.equal(budget.hardInputTokens, 212_500);
|
|
@@ -37,7 +35,33 @@ test('context budget reserves headroom and caps each ownership class', () => {
|
|
|
37
35
|
assert.equal(budget.maxToolResultChars, 24_000);
|
|
38
36
|
});
|
|
39
37
|
|
|
40
|
-
test('
|
|
38
|
+
test('large model capacity is independent from AgentSam working-set and pricing policy', () => {
|
|
39
|
+
const budget = createContextBudget({
|
|
40
|
+
windowTokens: 1_050_000,
|
|
41
|
+
targetInputTokens: 120_000,
|
|
42
|
+
compactAtTokens: 180_000,
|
|
43
|
+
interveneAtTokens: 220_000,
|
|
44
|
+
maxNormalInputTokens: 250_000,
|
|
45
|
+
pricingThresholdTokens: 272_000,
|
|
46
|
+
safetyMarginTokens: 22_000,
|
|
47
|
+
});
|
|
48
|
+
assert.equal(budget.windowTokens, 1_050_000);
|
|
49
|
+
assert.equal(budget.targetInputTokens, 120_000);
|
|
50
|
+
assert.equal(budget.compactAtTokens, 180_000);
|
|
51
|
+
assert.equal(budget.interveneAtTokens, 220_000);
|
|
52
|
+
assert.equal(budget.maxNormalInputTokens, 250_000);
|
|
53
|
+
assert.equal(budget.pricingThresholdTokens, 272_000);
|
|
54
|
+
assert.ok(budget.targetInputRatio < 0.12);
|
|
55
|
+
|
|
56
|
+
assert.equal(assessContextUsage(179_999, budget).shouldCompact, false);
|
|
57
|
+
assert.equal(assessContextUsage(180_000, budget).shouldCompact, true);
|
|
58
|
+
assert.equal(assessContextUsage(272_000, budget).pricingThresholdCrossed, false);
|
|
59
|
+
const crossed = assessContextUsage(272_001, budget);
|
|
60
|
+
assert.equal(crossed.pricingThresholdCrossed, true);
|
|
61
|
+
assert.equal(crossed.tokensUntilPricingThreshold, -1);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('context.resolve separates composition classes and returns an economic receipt', () => {
|
|
41
65
|
const budget = createContextBudget({
|
|
42
66
|
windowTokens: 32_000,
|
|
43
67
|
maxEvidenceChars: 90,
|
|
@@ -49,6 +73,9 @@ test('context.resolve selects high-priority evidence and returns a receipt', ()
|
|
|
49
73
|
objective: 'fix terminal cwd persistence',
|
|
50
74
|
refs: ['repo:demo'],
|
|
51
75
|
budget,
|
|
76
|
+
instructionChars: 20,
|
|
77
|
+
toolSchemaChars: 12,
|
|
78
|
+
historyChars: 8,
|
|
52
79
|
items: [
|
|
53
80
|
{ ref: 'file:low', kind: 'file', priority: 1, content: 'L'.repeat(50) },
|
|
54
81
|
{ ref: 'tool:high', kind: 'tool_result', priority: 10, content: 'T'.repeat(50) },
|
|
@@ -61,17 +88,39 @@ test('context.resolve selects high-priority evidence and returns a receipt', ()
|
|
|
61
88
|
assert.equal(pack.items[0].chars, 30);
|
|
62
89
|
assert.equal(pack.items[1].chars, 50);
|
|
63
90
|
assert.equal(pack.items[2].chars, 10);
|
|
64
|
-
assert.equal(pack.receipt.
|
|
91
|
+
assert.equal(pack.receipt.tool_result_chars, 30);
|
|
92
|
+
assert.equal(pack.receipt.evidence_chars, 60);
|
|
93
|
+
assert.equal(pack.receipt.instruction_chars, 20);
|
|
94
|
+
assert.equal(pack.receipt.tool_schema_chars, 12);
|
|
95
|
+
assert.equal(pack.receipt.history_chars, 8);
|
|
96
|
+
assert.equal(pack.receipt.chars, 130);
|
|
97
|
+
assert.equal(pack.receipt.estimate_kind, 'local');
|
|
98
|
+
assert.equal(pack.receipt.window_tokens, 32_000);
|
|
65
99
|
assert.equal(pack.receipt.sources_considered, 4);
|
|
66
100
|
assert.equal(pack.receipt.sources_included, 3);
|
|
67
|
-
assert.ok(pack.receipt.sources_deferred >= 3);
|
|
101
|
+
assert.ok(pack.receipt.sources_deferred >= 3);
|
|
68
102
|
assert.ok(pack.receipt.deferred_refs.includes('memory:later'));
|
|
103
|
+
assert.deepEqual(pack.receipt.rehydratable_refs, ['tool:high', 'file:high', 'file:low']);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('AgentSam instructions compile in stable then repository-specific precedence', t => {
|
|
107
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-instructions-'));
|
|
108
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
109
|
+
fs.mkdirSync(path.join(root, '.git'));
|
|
110
|
+
fs.writeFileSync(path.join(root, 'AGENTSAM.md'), '# Stable\nbase law\n');
|
|
111
|
+
fs.writeFileSync(path.join(root, '.agentsamrules'), '# Repo\nlocal law\n');
|
|
112
|
+
const compiled = compileAgentInstructions(root);
|
|
113
|
+
assert.deepEqual(compiled.precedence, ['AGENTSAM.md', '.agentsamrules']);
|
|
114
|
+
assert.equal(compiled.sources.length, 2);
|
|
115
|
+
assert.ok(compiled.content.indexOf('base law') < compiled.content.indexOf('local law'));
|
|
69
116
|
});
|
|
70
117
|
|
|
71
|
-
test('project context
|
|
118
|
+
test('project context loads bounded compiled AgentSam instructions', t => {
|
|
72
119
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-context-rules-'));
|
|
73
120
|
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
74
|
-
fs.
|
|
121
|
+
fs.mkdirSync(path.join(root, '.git'));
|
|
122
|
+
fs.writeFileSync(path.join(root, 'AGENTSAM.md'), '# stable\n' + 'a'.repeat(300));
|
|
123
|
+
fs.writeFileSync(path.join(root, '.agentsamrules'), '# rules\n' + 'x'.repeat(5000) + '\n');
|
|
75
124
|
const pack = resolveProjectContext({
|
|
76
125
|
cwd: root,
|
|
77
126
|
objective: 'inspect repository',
|
|
@@ -84,7 +133,7 @@ test('project context automatically loads bounded .agentsamrules as system conte
|
|
|
84
133
|
assert.equal(pack.receipt.system_chars, 1_000);
|
|
85
134
|
});
|
|
86
135
|
|
|
87
|
-
test('consumed tool results compact to 4k while preserving
|
|
136
|
+
test('consumed tool results compact to 4k while preserving rehydration identity', async () => {
|
|
88
137
|
const { compactConsumedToolResult } = await import('../src/context/index.js');
|
|
89
138
|
const item = compactConsumedToolResult('x'.repeat(10_000), { ref: 'tool:call_123', hash: 'sha256:abc' });
|
|
90
139
|
assert.equal(item.ref, 'tool:call_123');
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
import { scanTextForSecrets } from '../src/lib/deploy/secret-scan.js';
|
|
4
|
+
import { HEALTH_USER_AGENT, parseWranglerVersionId, probeDeployHealth, resolveHealthOrigin } from '../src/lib/deploy/health.js';
|
|
5
|
+
|
|
6
|
+
describe('deploy secret scan', () => {
|
|
7
|
+
it('allows fixture oauth values', () => {
|
|
8
|
+
const findings = scanTextForSecrets(
|
|
9
|
+
'CLOUDFLARE_OAUTH_CLIENT_ID=sillynotreal\nCLOUDFLARE_OAUTH_CLIENT_SECRET=sillynotreal-secret\n',
|
|
10
|
+
{ filename: 'apps/local-studio/.env.cloudflare.example' },
|
|
11
|
+
);
|
|
12
|
+
assert.equal(findings.length, 0);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('flags live openai-like keys', () => {
|
|
16
|
+
const findings = scanTextForSecrets('OPENAI_API_KEY=sk-abcdefghijklmnopqrstuvwxyz012345');
|
|
17
|
+
assert.equal(findings.some((f) => f.label === 'openai-like'), true);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('deploy health', () => {
|
|
22
|
+
it('parses wrangler version ids', () => {
|
|
23
|
+
assert.equal(
|
|
24
|
+
parseWranglerVersionId('Current Version ID: e9747d3a-4db6-4e02-b6fe-aa5a47f1588b'),
|
|
25
|
+
'e9747d3a-4db6-4e02-b6fe-aa5a47f1588b',
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('reads hostname from wrangler.jsonc', () => {
|
|
30
|
+
const origin = resolveHealthOrigin({
|
|
31
|
+
env: {},
|
|
32
|
+
wranglerConfigText: '{"routes":[{"pattern":"agentsam.inneranimalmedia.com","custom_domain":true}]}',
|
|
33
|
+
});
|
|
34
|
+
assert.equal(origin, 'https://agentsam.inneranimalmedia.com');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('treats /health ok:true with connector unconfigured as healthy', async () => {
|
|
38
|
+
const fetchImpl = async (url) => {
|
|
39
|
+
if (String(url).endsWith('/health')) {
|
|
40
|
+
return {
|
|
41
|
+
status: 200,
|
|
42
|
+
json: async () => ({ ok: true, connections: { cloudflare: { configured: false } } }),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { status: 200, json: async () => ({}) };
|
|
46
|
+
};
|
|
47
|
+
const health = await probeDeployHealth('https://agentsam.inneranimalmedia.com', { fetchImpl });
|
|
48
|
+
assert.equal(health.ok, true);
|
|
49
|
+
assert.equal(health.results['/health'].appOk, true);
|
|
50
|
+
assert.equal(health.results['/health'].cloudflareConfigured, false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('sends a browser-safe User-Agent so WAF does not 403 the probe', async () => {
|
|
54
|
+
const seen = [];
|
|
55
|
+
const fetchImpl = async (url, init = {}) => {
|
|
56
|
+
seen.push({ url, init });
|
|
57
|
+
return {
|
|
58
|
+
status: 200,
|
|
59
|
+
json: async () => ({ ok: true, connections: { cloudflare: { configured: false } } }),
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
await probeDeployHealth('https://agentsam.inneranimalmedia.com', { fetchImpl, paths: ['/health'] });
|
|
63
|
+
assert.equal(seen.length, 1);
|
|
64
|
+
assert.equal(seen[0].init.headers['User-Agent'], HEALTH_USER_AGENT);
|
|
65
|
+
assert.match(seen[0].init.headers.Accept, /application\/json/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
ERROR_CODE,
|
|
5
|
+
ERROR_REASON,
|
|
6
|
+
canonicalCodeFromGrpcStatus,
|
|
7
|
+
classifyCloudflareFailure,
|
|
8
|
+
classifyOAuthFailure,
|
|
9
|
+
classifyOpenAIError,
|
|
10
|
+
createErrorEnvelope,
|
|
11
|
+
createOpenAIHttpError,
|
|
12
|
+
diagnosticFromError,
|
|
13
|
+
grpcStatusForCode,
|
|
14
|
+
} from '../src/errors/index.js';
|
|
15
|
+
import { createOpenAIResponsesAdapter } from '../src/providers/openai-responses.js';
|
|
16
|
+
|
|
17
|
+
function response(status, body, headers = {}) { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } }); }
|
|
18
|
+
|
|
19
|
+
test('OpenAI diagnostics preserve machine code, request id, retry metadata and redact secrets', () => {
|
|
20
|
+
const error = createOpenAIHttpError({
|
|
21
|
+
status: 429,
|
|
22
|
+
body: { error: { type: 'rate_limit_error', code: 'slow_down', message: 'slow down Bearer abc.def.ghi', param: null }, api_key: 'sk-secret-value-123456789' },
|
|
23
|
+
headers: new Headers({ 'retry-after': '2', 'x-request-id': 'req_123' }),
|
|
24
|
+
requestedServiceTier: 'fast',
|
|
25
|
+
});
|
|
26
|
+
assert.equal(error.diagnostic.http_status, 429);
|
|
27
|
+
assert.equal(error.diagnostic.type, 'rate_limit_error');
|
|
28
|
+
assert.equal(error.diagnostic.code, 'slow_down');
|
|
29
|
+
assert.equal(error.diagnostic.request_id, 'req_123');
|
|
30
|
+
assert.equal(error.diagnostic.retry_after_ms, 2000);
|
|
31
|
+
assert.equal(error.diagnostic.retriable, true);
|
|
32
|
+
assert.equal(error.diagnostic.retry_strategy, 'retry_after_backoff');
|
|
33
|
+
assert.equal(error.diagnostic.requested_service_tier, 'fast');
|
|
34
|
+
assert.ok(!JSON.stringify(error.diagnostic).includes('sk-secret-value'));
|
|
35
|
+
assert.ok(!error.diagnostic.message.includes('abc.def.ghi'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('billing and spend errors are explicitly non-retriable while provider overload is retriable', () => {
|
|
39
|
+
for (const code of ['credit_balance_exhausted', 'organization_spend_limit_exceeded', 'project_spend_limit_exceeded', 'organization_usage_limit_exceeded']) {
|
|
40
|
+
assert.equal(classifyOpenAIError({ status: 429, code }).retriable, false);
|
|
41
|
+
}
|
|
42
|
+
assert.deepEqual(classifyOpenAIError({ status: 503, code: 'server_is_overloaded' }), { category: 'provider_overload', retriable: true, retry_strategy: 'retry_after_backoff' });
|
|
43
|
+
assert.equal(classifyOpenAIError({ status: 401 }).category, 'authentication');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('OpenAI retry classifier distinguishes configuration, continuation, rate, server, and operator-action failures', () => {
|
|
47
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, message: 'Invalid service_tier argument' }), { category: 'service_tier', retriable: false, retry_strategy: 'change_configuration' });
|
|
48
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, code: 'previous_response_not_found' }), { category: 'continuation', retriable: true, retry_strategy: 'retry_full_context' });
|
|
49
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, code: 'websocket_connection_limit_reached' }), { category: 'connection_lifetime', retriable: true, retry_strategy: 'reconnect' });
|
|
50
|
+
assert.deepEqual(classifyOpenAIError({ status: 429, type: 'rate_limit_error', code: 'slow_down' }), { category: 'ramp_rate', retriable: true, retry_strategy: 'retry_after_backoff' });
|
|
51
|
+
assert.deepEqual(classifyOpenAIError({ status: 500 }), { category: 'provider_server', retriable: true, retry_strategy: 'retry_backoff' });
|
|
52
|
+
assert.deepEqual(classifyOpenAIError({ status: 403 }), { category: 'authorization_or_region', retriable: false, retry_strategy: 'operator_action' });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('Responses adapter throws a diagnostic error instead of flattening the provider response', async () => {
|
|
56
|
+
const adapter = createOpenAIResponsesAdapter({
|
|
57
|
+
apiKey: 'sk-test',
|
|
58
|
+
fetchImpl: async () => response(429, { error: { type: 'insufficient_quota', code: 'project_spend_limit_exceeded', message: 'budget reached' } }, { 'x-request-id': 'req_budget' }),
|
|
59
|
+
});
|
|
60
|
+
await assert.rejects(
|
|
61
|
+
adapter.create({ model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default', input: 'hi' }),
|
|
62
|
+
(error) => {
|
|
63
|
+
const d = diagnosticFromError(error);
|
|
64
|
+
assert.equal(d.code, 'project_spend_limit_exceeded');
|
|
65
|
+
assert.equal(d.request_id, 'req_budget');
|
|
66
|
+
assert.equal(d.retriable, false);
|
|
67
|
+
return true;
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
test('canonical error contract preserves transport-neutral code and reason mappings', () => {
|
|
74
|
+
const envelope = createErrorEnvelope({
|
|
75
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
76
|
+
reason: ERROR_REASON.TUNNEL_CONNECTOR_UNAVAILABLE,
|
|
77
|
+
message: 'Local terminal tunnel has no healthy connector.',
|
|
78
|
+
retryable: true,
|
|
79
|
+
http_status: 530,
|
|
80
|
+
provider: 'cloudflare',
|
|
81
|
+
provider_code: '1033',
|
|
82
|
+
transport: 'cloudflare_tunnel',
|
|
83
|
+
});
|
|
84
|
+
assert.equal(envelope.ok, false);
|
|
85
|
+
assert.equal(envelope.code, 'UNAVAILABLE');
|
|
86
|
+
assert.equal(envelope.reason, 'tunnel_connector_unavailable');
|
|
87
|
+
assert.equal(envelope.http_status, 530);
|
|
88
|
+
assert.equal(envelope.grpc_status, 14);
|
|
89
|
+
assert.equal(envelope.retryable, true);
|
|
90
|
+
assert.equal(canonicalCodeFromGrpcStatus(14), ERROR_CODE.UNAVAILABLE);
|
|
91
|
+
assert.equal(grpcStatusForCode(ERROR_CODE.UNAUTHENTICATED), 16);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test('Cloudflare and OAuth boundary classifiers map native failures into canonical AgentSam semantics', () => {
|
|
95
|
+
assert.deepEqual(classifyCloudflareFailure({ httpStatus: 530, cloudflareCode: 1033 }), {
|
|
96
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
97
|
+
reason: ERROR_REASON.TUNNEL_CONNECTOR_UNAVAILABLE,
|
|
98
|
+
retryable: true,
|
|
99
|
+
});
|
|
100
|
+
assert.deepEqual(classifyCloudflareFailure({ httpStatus: 526 }), {
|
|
101
|
+
code: ERROR_CODE.UNAVAILABLE,
|
|
102
|
+
reason: ERROR_REASON.TLS_CERTIFICATE_INVALID,
|
|
103
|
+
retryable: false,
|
|
104
|
+
});
|
|
105
|
+
assert.deepEqual(classifyOAuthFailure('invalid_grant'), {
|
|
106
|
+
code: ERROR_CODE.UNAUTHENTICATED,
|
|
107
|
+
reason: ERROR_REASON.AUTH_INVALID,
|
|
108
|
+
retryable: false,
|
|
109
|
+
});
|
|
110
|
+
assert.deepEqual(classifyOAuthFailure('access_denied'), {
|
|
111
|
+
code: ERROR_CODE.PERMISSION_DENIED,
|
|
112
|
+
reason: ERROR_REASON.PERMISSION_DENIED,
|
|
113
|
+
retryable: false,
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import { evaluateContextFixture, listContextEvalFixtures } from '../src/eval/index.js';
|
|
6
|
+
|
|
7
|
+
const repoRoot = path.resolve(new URL('..', import.meta.url).pathname);
|
|
8
|
+
|
|
9
|
+
test('context eval ships AgentSam-shaped deterministic fixtures and never calls a provider', async () => {
|
|
10
|
+
const fixtures = listContextEvalFixtures();
|
|
11
|
+
assert.ok(fixtures.length >= 6);
|
|
12
|
+
assert.ok(fixtures.includes('continuation-after-compaction'));
|
|
13
|
+
const report = await evaluateContextFixture({ fixture: 'exact-symbol-callers' });
|
|
14
|
+
assert.equal(report.live_provider_used, false);
|
|
15
|
+
assert.equal(report.strategies.length, 3);
|
|
16
|
+
assert.ok(report.strategies.every(row => row.result === 'PASS'));
|
|
17
|
+
assert.ok(report.winner);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('compaction fixture reaches a proactive pressure region and proves explicit rehydration', async () => {
|
|
21
|
+
const report = await evaluateContextFixture({ fixture: 'continuation-after-compaction', strategy: 'compact' });
|
|
22
|
+
const row = report.strategies[0];
|
|
23
|
+
assert.equal(row.result, 'PASS');
|
|
24
|
+
assert.ok(row.compacted_chars > 0);
|
|
25
|
+
assert.ok(row.rehydrated_refs.includes('tool:call_previous'));
|
|
26
|
+
assert.ok(row.active_context_tokens < row.pricing_threshold_tokens);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('CLI JSON context eval is stable and offline', () => {
|
|
30
|
+
const result = spawnSync(process.execPath, ['src/cli.js', 'eval', 'context', '--fixture', 'two-file-repair', '--strategy', 'discovery', '--json'], { cwd: repoRoot, encoding: 'utf8' });
|
|
31
|
+
assert.equal(result.status, 0, result.stderr);
|
|
32
|
+
const report = JSON.parse(result.stdout);
|
|
33
|
+
assert.equal(report.fixture, 'two-file-repair');
|
|
34
|
+
assert.equal(report.live_provider_used, false);
|
|
35
|
+
assert.equal(report.strategies[0].strategy, 'discovery');
|
|
36
|
+
assert.equal(report.strategies[0].result, 'PASS');
|
|
37
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 { grantExecutionApproval, isExecutionApproved, toolApprovalKey } from '../src/lib/execution-approvals.js';
|
|
7
|
+
|
|
8
|
+
function tempHome(t) {
|
|
9
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-approval-home-'));
|
|
10
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
11
|
+
return home;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test('execution approvals are exact-command and project scoped', t => {
|
|
15
|
+
const home = tempHome(t);
|
|
16
|
+
const one = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-project-a-'));
|
|
17
|
+
const two = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-project-b-'));
|
|
18
|
+
t.after(() => fs.rmSync(one, { recursive: true, force: true }));
|
|
19
|
+
t.after(() => fs.rmSync(two, { recursive: true, force: true }));
|
|
20
|
+
const key = toolApprovalKey('cloudflare.wrangler.native', { command: 'whoami' });
|
|
21
|
+
assert.equal(key, 'cloudflare.wrangler.native:whoami');
|
|
22
|
+
assert.equal(isExecutionApproved({ cwd: one, key }, { home }), false);
|
|
23
|
+
grantExecutionApproval({ cwd: one, key }, { home });
|
|
24
|
+
assert.equal(isExecutionApproved({ cwd: one, key }, { home }), true);
|
|
25
|
+
assert.equal(isExecutionApproved({ cwd: two, key }, { home }), false);
|
|
26
|
+
assert.equal(isExecutionApproved({ cwd: one, key: 'cloudflare.wrangler.native:versions.list' }, { home }), false);
|
|
27
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
process.once('message', payload => {
|
|
2
|
+
const generation = payload?.request?.generation_id || null;
|
|
3
|
+
const delay = generation === 'slow' ? 180 : 20;
|
|
4
|
+
setTimeout(() => {
|
|
5
|
+
if (generation === 'fail') {
|
|
6
|
+
process.send?.({ ok: false, error: 'fixture knowledge failure' });
|
|
7
|
+
} else {
|
|
8
|
+
process.send?.({ ok: true, result: {
|
|
9
|
+
operation: payload?.request?.operation || null,
|
|
10
|
+
repository_id: payload?.config?.repository_id || null,
|
|
11
|
+
generation_id: generation,
|
|
12
|
+
} });
|
|
13
|
+
}
|
|
14
|
+
setTimeout(() => process.exit(0), 5);
|
|
15
|
+
}, delay);
|
|
16
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
|
|
6
|
+
const repoRoot = path.resolve(new URL('../..', import.meta.url).pathname);
|
|
7
|
+
|
|
8
|
+
function run(...args) {
|
|
9
|
+
return spawnSync(process.execPath, ['src/cli.js', ...args], { cwd: repoRoot, encoding: 'utf8' });
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
test('agentsam help is concise, topic-aware, and deterministic outside a TTY', () => {
|
|
13
|
+
const help = run('help');
|
|
14
|
+
assert.equal(help.status, 0, help.stderr);
|
|
15
|
+
assert.doesNotMatch(help.stderr, /Unknown command/);
|
|
16
|
+
assert.match(help.stdout, /Agent Sam/);
|
|
17
|
+
assert.match(help.stdout, /Type normally to work with Agent Sam/);
|
|
18
|
+
assert.match(help.stdout, /agentsam help <topic>/);
|
|
19
|
+
assert.match(help.stdout, /press \/ for the command picker/i);
|
|
20
|
+
assert.doesNotMatch(help.stdout, /Inspect options:/);
|
|
21
|
+
|
|
22
|
+
const topic = run('help', 'runtime');
|
|
23
|
+
assert.equal(topic.status, 0, topic.stderr);
|
|
24
|
+
assert.match(topic.stdout, /Runtime \/ terminal/);
|
|
25
|
+
assert.match(topic.stdout, /agentsam connections/);
|
|
26
|
+
assert.match(topic.stdout, /agentsam deploy/);
|
|
27
|
+
|
|
28
|
+
const all = run('help', '--all');
|
|
29
|
+
assert.equal(all.status, 0, all.stderr);
|
|
30
|
+
assert.match(all.stdout, /Build \/ inspect/);
|
|
31
|
+
assert.match(all.stdout, /Create \/ extend/);
|
|
32
|
+
|
|
33
|
+
const flag = run('--help');
|
|
34
|
+
assert.equal(flag.status, 0, flag.stderr);
|
|
35
|
+
assert.match(flag.stdout, /agentsam help <topic>/);
|
|
36
|
+
assert.doesNotMatch(flag.stdout, /Inspect options:/);
|
|
37
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { defaultConfig } from '../../src/knowledge/config.js';
|
|
8
|
+
import { createKnowledgeJobEngine } from '../../src/knowledge/service/job-engine.js';
|
|
9
|
+
import { startKnowledgeHttpServer } from '../../src/knowledge/service/server.js';
|
|
10
|
+
import { startKnowledgeGrpcServer } from '../../src/knowledge/service/grpc-server.js';
|
|
11
|
+
import { createKnowledgeServiceClient } from '../../src/knowledge/service/client.js';
|
|
12
|
+
import { createKnowledgeGrpcClient } from '../../src/knowledge/service/grpc-client.js';
|
|
13
|
+
import { grpc } from '../../src/knowledge/service/grpc-codec.js';
|
|
14
|
+
|
|
15
|
+
const fixtureWorker = fileURLToPath(new URL('../fixtures/knowledge-rpc-worker.mjs', import.meta.url));
|
|
16
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
17
|
+
|
|
18
|
+
async function waitFor(client, id, wanted = new Set(['completed', 'failed'])) {
|
|
19
|
+
for (let i = 0; i < 200; i++) {
|
|
20
|
+
const job = await client.job(id);
|
|
21
|
+
if (wanted.has(job.status)) return job;
|
|
22
|
+
await sleep(10);
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Job ${id} did not reach ${[...wanted].join('/')}.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function collect(stream) {
|
|
28
|
+
const events = [];
|
|
29
|
+
for await (const event of stream) events.push(event);
|
|
30
|
+
return events;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('knowledge HTTP and gRPC adapters share auth, validation, idempotency, lifecycle, cancellation, and durable jobs', async t => {
|
|
34
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-rpc-'));
|
|
35
|
+
t.after(() => fs.rmSync(tmp, { recursive: true, force: true }));
|
|
36
|
+
const root = path.join(tmp, 'repo');
|
|
37
|
+
const stateDir = path.join(tmp, 'state');
|
|
38
|
+
fs.mkdirSync(path.join(root, 'src'), { recursive: true });
|
|
39
|
+
fs.writeFileSync(path.join(root, 'src', 'sample.js'), 'export const sample = true;\n');
|
|
40
|
+
const config = defaultConfig({ include: ['src'] });
|
|
41
|
+
const repositories = { sample: { root, config } };
|
|
42
|
+
const token = 'r'.repeat(64);
|
|
43
|
+
const engine = createKnowledgeJobEngine({ stateDir, repositories, workerPath: fixtureWorker, jobTimeoutMs: 2_000 });
|
|
44
|
+
const http = await startKnowledgeHttpServer({ engine, token, port: 0 });
|
|
45
|
+
const rpcServer = await startKnowledgeGrpcServer({ engine, token, port: 0 });
|
|
46
|
+
const httpClient = createKnowledgeServiceClient({ baseUrl: `http://127.0.0.1:${http.address.port}`, token });
|
|
47
|
+
const rpc = createKnowledgeGrpcClient({ target: rpcServer.target, token });
|
|
48
|
+
const unauthorized = createKnowledgeGrpcClient({ target: rpcServer.target, token: 'x'.repeat(64) });
|
|
49
|
+
t.after(async () => {
|
|
50
|
+
unauthorized.close();
|
|
51
|
+
rpc.close();
|
|
52
|
+
await rpcServer.close();
|
|
53
|
+
await http.close();
|
|
54
|
+
await engine.close();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const repos = await rpc.repositories();
|
|
58
|
+
assert.deepEqual(repos.repositories, ['sample']);
|
|
59
|
+
assert.equal(repos.repository_details[0].repository_id, config.repository_id);
|
|
60
|
+
assert.equal(repos.embeddings_enabled, false);
|
|
61
|
+
await assert.rejects(unauthorized.repositories(), error => error.status === 401 && error.grpcCode === grpc.status.UNAUTHENTICATED);
|
|
62
|
+
await assert.rejects(
|
|
63
|
+
rpc.submit({ repository: 'sample', operation: 'bogus' }),
|
|
64
|
+
error => error.status === 400 && error.grpcCode === grpc.status.INVALID_ARGUMENT,
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const request = { repository: 'sample', operation: 'index', generation_id: 'equivalent' };
|
|
68
|
+
const fromHttp = await httpClient.submit(request, 'cross-transport');
|
|
69
|
+
const fromGrpc = await rpc.submit(request, 'cross-transport');
|
|
70
|
+
assert.equal(fromGrpc.id, fromHttp.id);
|
|
71
|
+
await assert.rejects(
|
|
72
|
+
rpc.submit({ ...request, scope: 'different' }, 'cross-transport'),
|
|
73
|
+
error => error.status === 409 && error.grpcCode === grpc.status.ABORTED,
|
|
74
|
+
);
|
|
75
|
+
const completed = await waitFor(rpc, fromGrpc.id);
|
|
76
|
+
assert.equal(completed.status, 'completed');
|
|
77
|
+
assert.equal((await httpClient.job(fromGrpc.id)).result.repository_id, completed.result.repository_id);
|
|
78
|
+
|
|
79
|
+
const blocker = await rpc.submit({ repository: 'sample', operation: 'index', generation_id: 'slow' }, 'blocker');
|
|
80
|
+
await waitFor(rpc, blocker.id, new Set(['running']));
|
|
81
|
+
const watched = await rpc.submit({ repository: 'sample', operation: 'index', generation_id: 'watch' }, 'watched');
|
|
82
|
+
const lifecycle = await collect(rpc.watch(watched.id));
|
|
83
|
+
assert.deepEqual(lifecycle.map(event => event.job.status), ['queued', 'running', 'completed']);
|
|
84
|
+
assert.deepEqual(lifecycle.map(event => event.sequence), [1, 2, 3]);
|
|
85
|
+
|
|
86
|
+
const failed = await rpc.submit({ repository: 'sample', operation: 'index', generation_id: 'fail' }, 'failed');
|
|
87
|
+
const failureEvents = await collect(rpc.watch(failed.id));
|
|
88
|
+
assert.equal(failureEvents.at(-1).job.status, 'failed');
|
|
89
|
+
assert.match(failureEvents.at(-1).job.error, /fixture knowledge failure/);
|
|
90
|
+
|
|
91
|
+
const cancellable = await rpc.submit({ repository: 'sample', operation: 'index', generation_id: 'slow' }, 'cancel-watch');
|
|
92
|
+
const controller = new AbortController();
|
|
93
|
+
const iterator = rpc.watch(cancellable.id, { signal: controller.signal })[Symbol.asyncIterator]();
|
|
94
|
+
const first = await iterator.next();
|
|
95
|
+
assert.equal(first.done, false);
|
|
96
|
+
controller.abort();
|
|
97
|
+
const afterAbort = await iterator.next();
|
|
98
|
+
assert.equal(afterAbort.done, true);
|
|
99
|
+
assert.equal((await waitFor(rpc, cancellable.id)).status, 'completed');
|
|
100
|
+
|
|
101
|
+
const deadlineJob = await rpc.submit({ repository: 'sample', operation: 'index', generation_id: 'slow' }, 'deadline-watch');
|
|
102
|
+
let deadlineError = null;
|
|
103
|
+
try {
|
|
104
|
+
await collect(rpc.watch(deadlineJob.id, { timeoutMs: 15 }));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
deadlineError = error;
|
|
107
|
+
}
|
|
108
|
+
assert.ok(deadlineError);
|
|
109
|
+
assert.equal(deadlineError.grpcCode, grpc.status.DEADLINE_EXCEEDED);
|
|
110
|
+
assert.equal(deadlineError.status, 504);
|
|
111
|
+
assert.equal((await waitFor(rpc, deadlineJob.id)).status, 'completed');
|
|
112
|
+
});
|