@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,61 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { test } from 'node:test';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { spawnSync } from 'node:child_process';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const cli = fileURLToPath(new URL('../../src/cli.js', import.meta.url));
|
|
10
|
+
|
|
11
|
+
async function fixture(t) {
|
|
12
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'agentsam-merkle-cli-test-'));
|
|
13
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function write(root, name, content = name) {
|
|
18
|
+
await fs.mkdir(path.dirname(path.join(root, name)), { recursive: true });
|
|
19
|
+
await fs.writeFile(path.join(root, name), content);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(args, cwd) {
|
|
23
|
+
return spawnSync(process.execPath, [cli, 'merkle', ...args], {
|
|
24
|
+
cwd,
|
|
25
|
+
encoding: 'utf8',
|
|
26
|
+
timeout: 15000,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
test('CLI snapshot/verify/diff supports JSON, moved roots, and distinct mismatch/error exit codes', async (t) => {
|
|
31
|
+
const root = await fixture(t), moved = await fixture(t);
|
|
32
|
+
await write(root, 'a.txt', 'hello');
|
|
33
|
+
await write(moved, 'a.txt', 'hello');
|
|
34
|
+
|
|
35
|
+
const saved = run(['snapshot', '.', '--json'], root);
|
|
36
|
+
assert.equal(saved.status, 0, saved.stderr);
|
|
37
|
+
const manifest = JSON.parse(saved.stdout);
|
|
38
|
+
assert.ok(manifest.output.endsWith('merkle.json'));
|
|
39
|
+
|
|
40
|
+
const matching = run(['verify', manifest.output, '--root', moved, '--json'], root);
|
|
41
|
+
assert.equal(matching.status, 0, matching.stderr);
|
|
42
|
+
assert.equal(JSON.parse(matching.stdout).equal, true);
|
|
43
|
+
|
|
44
|
+
await write(moved, 'a.txt', 'changed');
|
|
45
|
+
await write(moved, 'b.txt', 'new');
|
|
46
|
+
const changed = run(['verify', manifest.output, '--root', moved, '--json'], root);
|
|
47
|
+
assert.equal(changed.status, 1, changed.stderr);
|
|
48
|
+
assert.deepEqual(JSON.parse(changed.stdout).stats, { unchanged: 0, modified: 1, added: 1, removed: 0 });
|
|
49
|
+
|
|
50
|
+
const diff = run(['diff', root, moved, '--json'], root);
|
|
51
|
+
assert.equal(diff.status, 1, diff.stderr);
|
|
52
|
+
|
|
53
|
+
const invalid = run(['root', '.', '--typo', '--json'], root);
|
|
54
|
+
assert.equal(invalid.status, 2);
|
|
55
|
+
assert.equal(invalid.stdout, '');
|
|
56
|
+
assert.match(JSON.parse(invalid.stderr).error, /Unknown option/);
|
|
57
|
+
|
|
58
|
+
const piped = run(['inspect', '.', '--tui'], root);
|
|
59
|
+
assert.equal(piped.status, 0, piped.stderr);
|
|
60
|
+
assert.ok(!piped.stdout.includes('\x1b'));
|
|
61
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { test } from 'node:test';
|
|
7
|
+
import { resolveMerklePersistenceIdentity } from '../../src/commands/merkle-persist.js';
|
|
8
|
+
|
|
9
|
+
async function gitFixture(t, remote = null) {
|
|
10
|
+
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'agentsam-merkle-identity-'));
|
|
11
|
+
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
|
12
|
+
execFileSync('git', ['init', '-q', root]);
|
|
13
|
+
if (remote) execFileSync('git', ['-C', root, 'remote', 'add', 'origin', remote]);
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('Merkle persistence identity uses authenticated session + Git provider identity', async (t) => {
|
|
18
|
+
const root = await gitFixture(t, 'git@github.com:SamPrimeaux/agentsam-sdk.git');
|
|
19
|
+
const identity = resolveMerklePersistenceIdentity(root, {
|
|
20
|
+
session: { account_id: 'au_test' },
|
|
21
|
+
projectConfig: { repository: { id: 'local:should-not-win' } },
|
|
22
|
+
});
|
|
23
|
+
assert.deepEqual(identity, {
|
|
24
|
+
accountId: 'au_test',
|
|
25
|
+
repositoryId: 'github:samprimeaux/agentsam-sdk',
|
|
26
|
+
repositoryIdentitySource: 'git',
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('local repositories fall back to committed project identity', async (t) => {
|
|
31
|
+
const root = await gitFixture(t);
|
|
32
|
+
const identity = resolveMerklePersistenceIdentity(root, {
|
|
33
|
+
session: { account_id: 'au_test' },
|
|
34
|
+
projectConfig: { repository: { id: 'local:fixture-repository' } },
|
|
35
|
+
});
|
|
36
|
+
assert.equal(identity.repositoryId, 'local:fixture-repository');
|
|
37
|
+
assert.equal(identity.repositoryIdentitySource, 'project_manifest');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('Merkle persistence refuses unauthenticated ownership', async (t) => {
|
|
41
|
+
const root = await gitFixture(t, 'https://github.com/SamPrimeaux/agentsam-sdk.git');
|
|
42
|
+
assert.throws(() => resolveMerklePersistenceIdentity(root, { session: null }), /agentsam_login_required_for_merkle_persistence/);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('Merkle persistence CLI has no account/repository ownership env shortcuts', async () => {
|
|
46
|
+
const source = await fs.readFile(new URL('../../src/commands/merkle-persist.js', import.meta.url), 'utf8');
|
|
47
|
+
assert.doesNotMatch(source, /AGENTSAM_ACCOUNT_ID|AGENTSAM_REPOSITORY_ID|--account-id|--repository-id/);
|
|
48
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { spawnSync } from 'node:child_process';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
|
|
8
|
+
const repoRoot = path.resolve(new URL('../..', import.meta.url).pathname);
|
|
9
|
+
|
|
10
|
+
test('agentsam env init creates provider profile and reusable source loader', { skip: process.platform === 'win32' }, t => {
|
|
11
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-env-cli-'));
|
|
12
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
13
|
+
const env = { ...process.env, HOME: home };
|
|
14
|
+
const result = spawnSync(process.execPath, ['src/cli.js', 'env', 'init', 'openai'], { cwd: repoRoot, env, encoding: 'utf8' });
|
|
15
|
+
assert.equal(result.status, 0, result.stderr);
|
|
16
|
+
assert.match(result.stdout, /source ~\/.agentsam\/load-agent-env\.sh openai/);
|
|
17
|
+
assert.equal(fs.statSync(path.join(home, '.agentsam', 'env.d', 'openai.env')).mode & 0o777, 0o600);
|
|
18
|
+
assert.equal(fs.statSync(path.join(home, '.agentsam', 'load-agent-env.sh')).mode & 0o777, 0o700);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
test('Cloudflare env init backfills exactly one Wrangler account without printing credentials', async t => {
|
|
23
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-env-cf-'));
|
|
24
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
25
|
+
const { runEnv } = await import('../../src/commands/env.js');
|
|
26
|
+
let output = '';
|
|
27
|
+
await runEnv(['init', 'cloudflare'], {
|
|
28
|
+
home, env: { HOME: home }, write: text => { output += text; },
|
|
29
|
+
spawnSyncImpl: () => ({ status: 0, stdout: JSON.stringify({ accounts: [{ id: '11111111111111111111111111111111', name: 'Example' }] }), stderr: '' }),
|
|
30
|
+
});
|
|
31
|
+
const profile = fs.readFileSync(path.join(home, '.agentsam', 'env.d', 'cloudflare.env'), 'utf8');
|
|
32
|
+
assert.match(profile, /ACCOUNT_ID="11111111111111111111111111111111"/);
|
|
33
|
+
assert.match(profile, /CLOUDFLARE_API_TOKEN=""/);
|
|
34
|
+
assert.match(output, /account detected\/configured/);
|
|
35
|
+
assert.doesNotMatch(output, /CLOUDFLARE_API_TOKEN=.*[^ ]/);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
test('generated loader exports the selected provider profile into the caller shell', { skip: process.platform === 'win32' }, t => {
|
|
40
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-env-source-'));
|
|
41
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
42
|
+
const env = { ...process.env, HOME: home };
|
|
43
|
+
let result = spawnSync(process.execPath, ['src/cli.js', 'env', 'init', 'openai'], { cwd: repoRoot, env, encoding: 'utf8' });
|
|
44
|
+
assert.equal(result.status, 0, result.stderr);
|
|
45
|
+
fs.writeFileSync(path.join(home, '.agentsam', 'env.d', 'openai.env'), 'export OPENAI_API_KEY="sentinel-provider-key"\
|
|
46
|
+
', { mode: 0o600 });
|
|
47
|
+
result = spawnSync('bash', ['-lc', 'source "$HOME/.agentsam/load-agent-env.sh" openai && test "$OPENAI_API_KEY" = "sentinel-provider-key"'], { env, encoding: 'utf8' });
|
|
48
|
+
assert.equal(result.status, 0, result.stderr);
|
|
49
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createProviderAdapter } from '../../src/providers/index.js';
|
|
4
|
+
|
|
5
|
+
function response(body, status = 200, headers = {}) {
|
|
6
|
+
return {
|
|
7
|
+
ok: status >= 200 && status < 300,
|
|
8
|
+
status,
|
|
9
|
+
headers: { get(name) { return headers[String(name).toLowerCase()] || null; } },
|
|
10
|
+
async json() { return body; },
|
|
11
|
+
async text() { return JSON.stringify(body); },
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('OpenAI-compatible adapter accepts provider-discovered records and preserves cache key', async () => {
|
|
16
|
+
let body;
|
|
17
|
+
const record = {
|
|
18
|
+
model_key: 'openai:account-model',
|
|
19
|
+
provider: 'openai',
|
|
20
|
+
provider_model_id: 'account-model',
|
|
21
|
+
context_window: 128000,
|
|
22
|
+
max_output_tokens: 8192,
|
|
23
|
+
reasoning_efforts: ['auto'],
|
|
24
|
+
service_tiers: ['default'],
|
|
25
|
+
capabilities: { responses: true },
|
|
26
|
+
pricing: null,
|
|
27
|
+
};
|
|
28
|
+
const provider = createProviderAdapter({
|
|
29
|
+
modelRecord: record,
|
|
30
|
+
credential: { value: 'key' },
|
|
31
|
+
fetchImpl: async (url, init) => {
|
|
32
|
+
assert.match(url, /\/v1\/responses$/);
|
|
33
|
+
body = JSON.parse(init.body);
|
|
34
|
+
return response({
|
|
35
|
+
id: 'resp_1',
|
|
36
|
+
status: 'completed',
|
|
37
|
+
output_text: 'hello',
|
|
38
|
+
output: [],
|
|
39
|
+
usage: { input_tokens: 12, output_tokens: 3, input_tokens_details: { cached_tokens: 5 } },
|
|
40
|
+
});
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const result = await provider.create({
|
|
44
|
+
model: record.provider_model_id,
|
|
45
|
+
modelRecord: record,
|
|
46
|
+
input: 'hi',
|
|
47
|
+
reasoningEffort: 'auto',
|
|
48
|
+
serviceTier: 'default',
|
|
49
|
+
promptCacheKey: 'session-1',
|
|
50
|
+
});
|
|
51
|
+
assert.equal(body.prompt_cache_key, 'session-1');
|
|
52
|
+
assert.equal(body.reasoning, undefined);
|
|
53
|
+
assert.equal(result.output_text, 'hello');
|
|
54
|
+
assert.equal(result.provider_state.previous_response_id, 'resp_1');
|
|
55
|
+
assert.equal(result.usage_snapshot.current_context.window_tokens, 128000);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
test('xAI adapter supports native compaction without requiring a live API key in tests', async () => {
|
|
61
|
+
const requests = [];
|
|
62
|
+
const record = {
|
|
63
|
+
model_key: 'grok:grok-test',
|
|
64
|
+
provider: 'grok',
|
|
65
|
+
provider_model_id: 'grok-test',
|
|
66
|
+
context_window: 256000,
|
|
67
|
+
max_output_tokens: 8192,
|
|
68
|
+
reasoning_efforts: ['auto'],
|
|
69
|
+
service_tiers: ['default'],
|
|
70
|
+
capabilities: { responses: true, compaction: true, prompt_caching: true },
|
|
71
|
+
pricing: null,
|
|
72
|
+
};
|
|
73
|
+
const provider = createProviderAdapter({
|
|
74
|
+
modelRecord: record,
|
|
75
|
+
credential: { value: 'xai-key' },
|
|
76
|
+
fetchImpl: async (url, init) => {
|
|
77
|
+
const body = JSON.parse(init.body);
|
|
78
|
+
requests.push({ url, body });
|
|
79
|
+
if (url.endsWith('/responses/compact')) {
|
|
80
|
+
return response({
|
|
81
|
+
id: 'cmp_1',
|
|
82
|
+
object: 'response.compaction',
|
|
83
|
+
output: [{ type: 'compaction', id: 'cmp_1', encrypted_content: 'opaque' }],
|
|
84
|
+
usage: { input_tokens: 1000, output_tokens: 120, input_tokens_details: { cached_tokens: 0 } },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return response({
|
|
88
|
+
id: 'resp_xai_1',
|
|
89
|
+
status: 'completed',
|
|
90
|
+
output_text: 'first answer',
|
|
91
|
+
output: [{ type: 'message', role: 'assistant', content: [{ type: 'output_text', text: 'first answer' }] }],
|
|
92
|
+
usage: { input_tokens: 50, output_tokens: 10, input_tokens_details: { cached_tokens: 20 } },
|
|
93
|
+
});
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const first = await provider.create({
|
|
98
|
+
modelRecord: record,
|
|
99
|
+
input: 'hello',
|
|
100
|
+
instructions: 'stable system prefix',
|
|
101
|
+
promptCacheKey: 'session-xai',
|
|
102
|
+
});
|
|
103
|
+
assert.equal(first.provider_state.previous_response_id, 'resp_xai_1');
|
|
104
|
+
assert.equal(Array.isArray(first.provider_state.compaction_input), true);
|
|
105
|
+
assert.equal(first.provider_state.compaction_input[0].role, 'system');
|
|
106
|
+
|
|
107
|
+
const compacted = await provider.compact({
|
|
108
|
+
modelRecord: record,
|
|
109
|
+
providerState: first.provider_state,
|
|
110
|
+
tokensBefore: 1000,
|
|
111
|
+
});
|
|
112
|
+
assert.match(requests.at(-1).url, /\/v1\/responses\/compact$/);
|
|
113
|
+
assert.equal(Array.isArray(requests.at(-1).body.input), true);
|
|
114
|
+
assert.equal(requests.at(-1).body.previous_response_id, undefined);
|
|
115
|
+
assert.deepEqual(compacted.provider_state.compaction_input, [
|
|
116
|
+
{ type: 'compaction', id: 'cmp_1', encrypted_content: 'opaque' },
|
|
117
|
+
]);
|
|
118
|
+
assert.equal(compacted.provider_state.previous_response_id, null);
|
|
119
|
+
|
|
120
|
+
await provider.create({
|
|
121
|
+
modelRecord: record,
|
|
122
|
+
providerState: compacted.provider_state,
|
|
123
|
+
input: 'continue after compaction',
|
|
124
|
+
promptCacheKey: 'session-xai',
|
|
125
|
+
});
|
|
126
|
+
assert.deepEqual(requests.at(-1).body.input, [
|
|
127
|
+
{ type: 'compaction', id: 'cmp_1', encrypted_content: 'opaque' },
|
|
128
|
+
{ role: 'user', content: 'continue after compaction' },
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
test('Anthropic adapter keeps append-only provider state and tool calls', async () => {
|
|
134
|
+
const record = {
|
|
135
|
+
model_key: 'anthropic:claude-test', provider: 'anthropic', provider_model_id: 'claude-test',
|
|
136
|
+
context_window: 200000, max_output_tokens: 4096, reasoning_efforts: ['auto'], service_tiers: ['default'], pricing: null,
|
|
137
|
+
};
|
|
138
|
+
const provider = createProviderAdapter({
|
|
139
|
+
modelRecord: record,
|
|
140
|
+
credential: { value: 'key' },
|
|
141
|
+
fetchImpl: async () => response({
|
|
142
|
+
id: 'msg_1', stop_reason: 'tool_use',
|
|
143
|
+
content: [{ type: 'tool_use', id: 'toolu_1', name: 'as_test', input: { a: 1 } }],
|
|
144
|
+
usage: { input_tokens: 20, output_tokens: 5, cache_read_input_tokens: 7 },
|
|
145
|
+
}),
|
|
146
|
+
});
|
|
147
|
+
const result = await provider.create({
|
|
148
|
+
modelRecord: record,
|
|
149
|
+
input: 'do it',
|
|
150
|
+
instructions: 'stable prefix',
|
|
151
|
+
tools: [{ type: 'function', name: 'as_test', description: 'test', parameters: { type: 'object', properties: {} } }],
|
|
152
|
+
});
|
|
153
|
+
assert.equal(result.tool_calls[0].call_id, 'toolu_1');
|
|
154
|
+
assert.equal(result.provider_state.messages.at(-1).role, 'assistant');
|
|
155
|
+
assert.equal(result.usage_snapshot.cumulative.cached_input_tokens, 7);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('Gemini adapter maps provider token limits and function calls into AgentSam shape', async () => {
|
|
159
|
+
const record = {
|
|
160
|
+
model_key: 'gemini:gemini-test', provider: 'gemini', provider_model_id: 'gemini-test',
|
|
161
|
+
context_window: 1000000, max_output_tokens: 8192, reasoning_efforts: ['auto'], service_tiers: ['default'], pricing: null,
|
|
162
|
+
};
|
|
163
|
+
const provider = createProviderAdapter({
|
|
164
|
+
modelRecord: record,
|
|
165
|
+
credential: { value: 'key' },
|
|
166
|
+
fetchImpl: async () => response({
|
|
167
|
+
responseId: 'gem_1',
|
|
168
|
+
candidates: [{ finishReason: 'STOP', content: { role: 'model', parts: [{ functionCall: { name: 'as_test', args: { x: 2 } } }] } }],
|
|
169
|
+
usageMetadata: { promptTokenCount: 30, candidatesTokenCount: 8, cachedContentTokenCount: 10, thoughtsTokenCount: 2 },
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
const result = await provider.create({ modelRecord: record, input: 'go', tools: [{ type: 'function', name: 'as_test', parameters: { type: 'object' } }] });
|
|
173
|
+
assert.equal(result.tool_calls[0].name, 'as_test');
|
|
174
|
+
assert.equal(result.usage_snapshot.current_context.window_tokens, 1000000);
|
|
175
|
+
assert.equal(result.usage_snapshot.cumulative.reasoning_tokens, 2);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test('Cloudflare adapter executes account-scoped Workers AI through the connected account', async () => {
|
|
179
|
+
let seen;
|
|
180
|
+
const record = {
|
|
181
|
+
model_key: 'cloudflare:@cf/test/model', provider: 'cloudflare', provider_model_id: '@cf/test/model',
|
|
182
|
+
context_window: null, max_output_tokens: null, reasoning_efforts: ['auto'], service_tiers: ['default'], pricing: null,
|
|
183
|
+
};
|
|
184
|
+
const provider = createProviderAdapter({
|
|
185
|
+
modelRecord: record,
|
|
186
|
+
credential: { value: 'cf-token', account_id: '1234567890abcdef1234567890abcdef' },
|
|
187
|
+
fetchImpl: async (url, init) => {
|
|
188
|
+
seen = { url, headers: init.headers, body: JSON.parse(init.body) };
|
|
189
|
+
return response({ id: 'cf_1', choices: [{ finish_reason: 'stop', message: { role: 'assistant', content: 'done' } }], usage: { prompt_tokens: 9, completion_tokens: 2 } });
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
const result = await provider.create({ modelRecord: record, input: 'go' });
|
|
193
|
+
assert.match(seen.url, /accounts\/1234567890abcdef1234567890abcdef\/ai\/v1\/chat\/completions$/);
|
|
194
|
+
assert.equal(seen.headers['cf-aig-gateway-id'], 'default');
|
|
195
|
+
assert.equal(seen.body.model, '@cf/test/model');
|
|
196
|
+
assert.equal(result.output_text, 'done');
|
|
197
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import {
|
|
6
|
+
COMPANY_REPOSITORY_GRAPH_SCHEMA_VERSION,
|
|
7
|
+
createRepositoryContract,
|
|
8
|
+
createRepositoryDependency,
|
|
9
|
+
createRepositoryIdentity,
|
|
10
|
+
} from '../../packages/agentsam-repository/src/contracts.js';
|
|
11
|
+
|
|
12
|
+
const root = path.resolve(import.meta.dirname, '../..');
|
|
13
|
+
|
|
14
|
+
function schema(relative) {
|
|
15
|
+
return JSON.parse(fs.readFileSync(path.join(root, relative), 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
test('portable repository identity is account-independent', () => {
|
|
19
|
+
const identity = createRepositoryIdentity({
|
|
20
|
+
repository_id: 'repo_agentsam_sdk',
|
|
21
|
+
full_name: 'SamPrimeaux/agentsam-sdk',
|
|
22
|
+
role: 'contract/runtime SDK',
|
|
23
|
+
default_branch: 'main',
|
|
24
|
+
});
|
|
25
|
+
assert.equal(identity.schema_version, COMPANY_REPOSITORY_GRAPH_SCHEMA_VERSION);
|
|
26
|
+
assert.equal(identity.repository_id, 'repo_agentsam_sdk');
|
|
27
|
+
assert.equal(Object.hasOwn(identity, 'account_id'), false);
|
|
28
|
+
assert.throws(() => createRepositoryIdentity({
|
|
29
|
+
repository_id: 'repo_x', full_name: 'owner/repo', role: 'test', account_id: 'au_test',
|
|
30
|
+
}), /must not embed account_id/);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('repository contracts use account and repository SSOT only', () => {
|
|
34
|
+
const record = createRepositoryContract({
|
|
35
|
+
id: 'rct_runtime_receipts_v2',
|
|
36
|
+
account_id: 'au_test',
|
|
37
|
+
repository_id: 'repo_agentsam_sdk',
|
|
38
|
+
contract_key: 'runtime-receipts',
|
|
39
|
+
contract_version: '2',
|
|
40
|
+
contract_type: 'schema',
|
|
41
|
+
name: 'Runtime receipts',
|
|
42
|
+
manifest_path: 'protocol/telemetry',
|
|
43
|
+
contract_hash: `sha256:${'1'.repeat(64)}`,
|
|
44
|
+
});
|
|
45
|
+
assert.equal(record.account_id, 'au_test');
|
|
46
|
+
assert.equal(record.contract_type, 'schema');
|
|
47
|
+
assert.equal(record.status, 'active');
|
|
48
|
+
assert.throws(() => createRepositoryContract({
|
|
49
|
+
...record, id: 'bad', workspace_id: 'ws_old',
|
|
50
|
+
}), /legacy ownership field/);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('repository dependencies encode cross-repo criticality and failure policy', () => {
|
|
54
|
+
const edge = createRepositoryDependency({
|
|
55
|
+
id: 'rdep_iam_execos',
|
|
56
|
+
account_id: 'au_test',
|
|
57
|
+
source_repository_id: 'repo_inneranimalmedia',
|
|
58
|
+
target_repository_id: 'repo_execos',
|
|
59
|
+
dependency_type: 'runtime',
|
|
60
|
+
criticality: 'critical',
|
|
61
|
+
required_version: '1',
|
|
62
|
+
required_contract_hash: `sha256:${'2'.repeat(64)}`,
|
|
63
|
+
failure_policy: 'block_deploy',
|
|
64
|
+
});
|
|
65
|
+
assert.equal(edge.dependency_type, 'runtime');
|
|
66
|
+
assert.equal(edge.criticality, 'critical');
|
|
67
|
+
assert.equal(edge.failure_policy, 'block_deploy');
|
|
68
|
+
assert.throws(() => createRepositoryDependency({ ...edge, criticality: 'whatever' }), /unsupported criticality/);
|
|
69
|
+
assert.throws(() => createRepositoryDependency({ ...edge, owner_user_id: 'usr_old' }), /legacy ownership field/);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('published JSON schemas expose the same ownership boundary', () => {
|
|
73
|
+
const identity = schema('protocol/repository/repository-identity.schema.json');
|
|
74
|
+
const contract = schema('protocol/repository/repository-contract.schema.json');
|
|
75
|
+
const dependency = schema('protocol/repository/repository-dependency.schema.json');
|
|
76
|
+
|
|
77
|
+
assert.equal(identity.additionalProperties, false);
|
|
78
|
+
assert.equal(Object.hasOwn(identity.properties, 'account_id'), false);
|
|
79
|
+
assert.ok(contract.required.includes('account_id'));
|
|
80
|
+
assert.ok(contract.required.includes('repository_id'));
|
|
81
|
+
assert.ok(dependency.required.includes('account_id'));
|
|
82
|
+
assert.ok(dependency.required.includes('source_repository_id'));
|
|
83
|
+
assert.ok(dependency.required.includes('target_repository_id'));
|
|
84
|
+
|
|
85
|
+
for (const item of [identity, contract, dependency]) {
|
|
86
|
+
for (const legacy of ['tenant_id', 'workspace_id', 'user_id', 'owner_user_id']) {
|
|
87
|
+
assert.equal(Object.hasOwn(item.properties, legacy), false, `${legacy} must not be part of ${item.title}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
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 { createLocalSqliteDatabase } from '../../src/local/sqlite.js';
|
|
7
|
+
import { applyRuntimeMigrations } from '../../src/local/migrations.js';
|
|
8
|
+
|
|
9
|
+
test('portable runtime migration installs AgentSam CLI state tables idempotently', async (t) => {
|
|
10
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-runtime-migration-'));
|
|
11
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
12
|
+
const db = await createLocalSqliteDatabase(path.join(root, 'agentsam.sqlite'));
|
|
13
|
+
try {
|
|
14
|
+
const first = await applyRuntimeMigrations(db);
|
|
15
|
+
assert.equal(first.applied, 1);
|
|
16
|
+
|
|
17
|
+
const tables = await db.prepare(
|
|
18
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'agentsam_%' ORDER BY name"
|
|
19
|
+
).all();
|
|
20
|
+
const names = new Set(tables.results.map((row) => row.name));
|
|
21
|
+
for (const name of [
|
|
22
|
+
'agentsam_agent_run',
|
|
23
|
+
'agentsam_approval_queue',
|
|
24
|
+
'agentsam_compaction_events',
|
|
25
|
+
'agentsam_context_digest',
|
|
26
|
+
'agentsam_cron_runs',
|
|
27
|
+
'agentsam_plans',
|
|
28
|
+
'agentsam_schema_migrations',
|
|
29
|
+
'agentsam_todo',
|
|
30
|
+
]) assert.equal(names.has(name), true, name);
|
|
31
|
+
|
|
32
|
+
const timers = await db.prepare(
|
|
33
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='active_timers'"
|
|
34
|
+
).first();
|
|
35
|
+
assert.equal(timers.name, 'active_timers');
|
|
36
|
+
|
|
37
|
+
const second = await applyRuntimeMigrations(db);
|
|
38
|
+
assert.equal(second.applied, 0);
|
|
39
|
+
assert.equal(second.results[0].status, 'already_applied');
|
|
40
|
+
} finally {
|
|
41
|
+
db.close();
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('ephemeral context digests expire and refresh while durable digests stay durable', async (t) => {
|
|
46
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-digest-migration-'));
|
|
47
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
48
|
+
const db = await createLocalSqliteDatabase(path.join(root, 'agentsam.sqlite'));
|
|
49
|
+
try {
|
|
50
|
+
await applyRuntimeMigrations(db);
|
|
51
|
+
|
|
52
|
+
await db.prepare(
|
|
53
|
+
"INSERT INTO agentsam_context_digest (id,digest_type,source_hash,digest_hash,digest_text) VALUES (?,?,?,?,?)"
|
|
54
|
+
).bind('repo_1', 'repo', 's1', 'h1', 'durable').run();
|
|
55
|
+
const durable = await db.prepare(
|
|
56
|
+
'SELECT expires_at_unix FROM agentsam_context_digest WHERE id=?'
|
|
57
|
+
).bind('repo_1').first();
|
|
58
|
+
assert.equal(durable.expires_at_unix, null);
|
|
59
|
+
|
|
60
|
+
await db.prepare(
|
|
61
|
+
"INSERT INTO agentsam_context_digest (id,digest_type,source_hash,digest_hash,digest_text) VALUES (?,?,?,?,?)"
|
|
62
|
+
).bind('session_1', 'session', 's2', 'h2', 'ephemeral').run();
|
|
63
|
+
const initial = await db.prepare(
|
|
64
|
+
'SELECT expires_at_unix FROM agentsam_context_digest WHERE id=?'
|
|
65
|
+
).bind('session_1').first();
|
|
66
|
+
assert.equal(Number(initial.expires_at_unix) > 0, true);
|
|
67
|
+
|
|
68
|
+
await db.prepare(
|
|
69
|
+
'UPDATE agentsam_context_digest SET expires_at_unix = unixepoch() + 60 WHERE id=?'
|
|
70
|
+
).bind('session_1').run();
|
|
71
|
+
await db.prepare(
|
|
72
|
+
'UPDATE agentsam_context_digest SET hit_count = hit_count + 1 WHERE id=?'
|
|
73
|
+
).bind('session_1').run();
|
|
74
|
+
const refreshed = await db.prepare(
|
|
75
|
+
'SELECT expires_at_unix, hit_count FROM agentsam_context_digest WHERE id=?'
|
|
76
|
+
).bind('session_1').first();
|
|
77
|
+
assert.equal(refreshed.hit_count, 1);
|
|
78
|
+
assert.equal(Number(refreshed.expires_at_unix) > Math.floor(Date.now() / 1000) + 2_500_000, true);
|
|
79
|
+
} finally {
|
|
80
|
+
db.close();
|
|
81
|
+
}
|
|
82
|
+
});
|
|
@@ -88,6 +88,11 @@ test('portable dockerize stages only SDK runtime, preserves identities/tokens, a
|
|
|
88
88
|
const context = path.join(result.written.versionDir, 'context');
|
|
89
89
|
assert.equal(fs.existsSync(path.join(context, 'customer-secret.js')), false);
|
|
90
90
|
assert.equal(fs.existsSync(path.join(context, 'src/knowledge/service/server.js')), true);
|
|
91
|
+
assert.equal(fs.existsSync(path.join(context, 'src/knowledge/service/grpc-server.js')), true);
|
|
92
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/knowledge_pb.js')), true);
|
|
93
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/knowledge_grpc_pb.js')), true);
|
|
94
|
+
assert.equal(fs.existsSync(path.join(context, 'src/rpc/generated/package.json')), true);
|
|
95
|
+
assert.equal(fs.existsSync(path.join(context, 'src/errors/contract.js')), true);
|
|
91
96
|
assert.equal(fs.existsSync(path.join(context, 'package-lock.json')), true);
|
|
92
97
|
const token = fs.readFileSync(result.tokenFile, 'utf8');
|
|
93
98
|
assert.equal(fs.statSync(result.tokenFile).mode & 0o777, 0o600);
|
package/test/knowledge.test.mjs
CHANGED
|
@@ -114,6 +114,22 @@ test('source safety and setup preserve repository files; read-only plan creates
|
|
|
114
114
|
assert.ok(!paths.includes('.env.js')); assert.ok(!paths.includes('ignored/hidden.ts'));
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test('index and search derive safe local defaults when knowledge.json is absent', async t => {
|
|
118
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-configless-'));
|
|
119
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
120
|
+
execFileSync('git', ['init', '-q'], { cwd: root });
|
|
121
|
+
execFileSync('git', ['remote', 'add', 'origin', 'git@github.com:ExampleOrg/ConfiglessRepo.git'], { cwd: root });
|
|
122
|
+
write(root, 'src/real.ts', 'export function configlessSymbol() { return 7; }\n');
|
|
123
|
+
const run = args => JSON.parse(execFileSync(process.execPath, [cli, ...args], { cwd: root, encoding: 'utf8' }));
|
|
124
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
125
|
+
const plan = run(['index', 'plan']);
|
|
126
|
+
assert.ok(plan.files >= 1); assert.equal(plan.embedding_inputs, 0);
|
|
127
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
128
|
+
assert.equal(run(['index', 'run']).published, true);
|
|
129
|
+
assert.equal(fs.existsSync(path.join(root, '.agentsam/knowledge.json')), false);
|
|
130
|
+
assert.equal(run(['search', 'configlessSymbol']).hits[0].path, 'src/real.ts');
|
|
131
|
+
});
|
|
132
|
+
|
|
117
133
|
test('two independent repositories work through the same CLI and exported package', async t => {
|
|
118
134
|
for (const name of ['customer-a', 'customer-b']) {
|
|
119
135
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), name));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { WebSocket } from 'ws';
|
|
4
|
+
|
|
5
|
+
const liveUrl = String(process.env.AGENTSAM_LIVE_TERMINAL_WS_URL || '').trim();
|
|
6
|
+
|
|
7
|
+
test('live terminal transport accepts a websocket connection', { skip: !liveUrl }, async (t) => {
|
|
8
|
+
const ws = new WebSocket(liveUrl, {
|
|
9
|
+
headers: process.env.AGENTSAM_LIVE_TERMINAL_AUTH
|
|
10
|
+
? { authorization: process.env.AGENTSAM_LIVE_TERMINAL_AUTH }
|
|
11
|
+
: undefined,
|
|
12
|
+
});
|
|
13
|
+
t.after(() => { try { ws.close(); } catch {} });
|
|
14
|
+
|
|
15
|
+
await Promise.race([
|
|
16
|
+
new Promise((resolve, reject) => {
|
|
17
|
+
ws.once('open', resolve);
|
|
18
|
+
ws.once('error', reject);
|
|
19
|
+
}),
|
|
20
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('live terminal websocket open timed out')), 8000)),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
assert.equal(ws.readyState, WebSocket.OPEN);
|
|
24
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
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 { createLocalSession, listLocalSessions, loadLocalSession, saveLocalSession, sessionTitleFromInput } from '../src/lib/local-sessions.js';
|
|
7
|
+
import { renderSessionReceipt } from '../src/commands/shell.js';
|
|
8
|
+
|
|
9
|
+
function tempHome(t) {
|
|
10
|
+
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-session-home-'));
|
|
11
|
+
t.after(() => fs.rmSync(home, { recursive: true, force: true }));
|
|
12
|
+
return home;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
test('local sessions persist provider-neutral continuation, usage, cost, and last-input title', t => {
|
|
16
|
+
const home = tempHome(t);
|
|
17
|
+
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-session-project-'));
|
|
18
|
+
t.after(() => fs.rmSync(cwd, { recursive: true, force: true }));
|
|
19
|
+
let session = createLocalSession({ cwd, last_input: 'Run wrangler whoami' }, { home });
|
|
20
|
+
assert.match(session.id, /^asess_[0-9a-f-]{36}$/i);
|
|
21
|
+
session = saveLocalSession({
|
|
22
|
+
...session,
|
|
23
|
+
title: sessionTitleFromInput('Run wrangler whoami'),
|
|
24
|
+
model_key: 'openai:gpt-6-astra',
|
|
25
|
+
actual_service_tier: 'fast',
|
|
26
|
+
provider_state: { provider: 'openai', previous_response_id: 'resp_123' },
|
|
27
|
+
usage_snapshot: { current_context: { input_tokens: 21_244, window_tokens: 1_050_000 } },
|
|
28
|
+
cumulative_usage: { input_tokens: 21_244, cached_input_tokens: 60_544, output_tokens: 219, reasoning_tokens: 31 },
|
|
29
|
+
total_cost_usd: 0.123456,
|
|
30
|
+
cost_breakdown_usd: { input: 0.08, cached_input: 0.01, cache_write: 0.003456, output: 0.03 },
|
|
31
|
+
status: 'paused',
|
|
32
|
+
active_elapsed_ms: 90_000,
|
|
33
|
+
active_started_at: null,
|
|
34
|
+
}, { home });
|
|
35
|
+
|
|
36
|
+
const loaded = loadLocalSession(session.id, { home });
|
|
37
|
+
assert.equal(loaded.provider_state.previous_response_id, 'resp_123');
|
|
38
|
+
assert.equal(loaded.cumulative_usage.cached_input_tokens, 60_544);
|
|
39
|
+
assert.equal(listLocalSessions({ home, cwd })[0].id, session.id);
|
|
40
|
+
|
|
41
|
+
const receipt = renderSessionReceipt(loaded);
|
|
42
|
+
assert.match(receipt, /Token usage: total=21,463 input=21,244 \(\+ 60,544 cached\) output=219 reasoning=31/);
|
|
43
|
+
assert.match(receipt, /Spent: \$0\.1235/);
|
|
44
|
+
assert.match(receipt, /Elapsed: 1m 30s/);
|
|
45
|
+
assert.match(receipt, /Cost breakdown: input \$0\.0800 · cached \$0\.0100 · cache write \$0\.003456 · output \$0\.0300/);
|
|
46
|
+
assert.match(receipt, new RegExp(`agentsam resume ${session.id}`));
|
|
47
|
+
assert.match(receipt, /Run wrangler whoami/);
|
|
48
|
+
});
|