@inneranimalmedia/agentsam-sdk 2.4.0 → 2.5.0
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/README.md +7 -6
- package/docs/AGENTSAM_WORKBENCH.md +30 -0
- package/docs/AUTH_IDENTITY_CONTRACT.md +52 -0
- package/docs/CAPABILITIES.md +5 -3
- package/docs/CLI_SHELL.md +57 -80
- package/docs/CMS_STUDIO.md +101 -0
- package/docs/CONTEXT.md +170 -0
- package/docs/LOCAL_OLLAMA.md +58 -0
- package/docs/MERKLE.md +19 -19
- package/docs/PORTABLE_CONTEXT.md +4 -3
- package/docs/PROJECT_CONFIG.md +72 -0
- package/docs/RELEASES.md +26 -2
- package/docs/REPOSITORY_INTELLIGENCE.md +1 -1
- package/docs/REPOSITORY_KNOWLEDGE.md +114 -0
- package/docs/SDK_WORKER.md +86 -0
- package/docs/SECURITY.md +59 -22
- package/docs/client-cms-editor.md +15 -0
- package/docs/local-studio/WORKMODE_DONOR_NOTES.md +485 -0
- package/package.json +17 -5
- package/packages/identity/package.json +11 -2
- package/packages/identity/src/contracts/auth-config.js +98 -0
- package/packages/identity/src/index.js +1 -0
- package/packages/identity/src/oauth/README.md +2 -2
- package/packages/identity/src/oauth/credentials.js +11 -4
- package/packages/identity/src/oauth/iam-platform.js +3 -3
- package/packages/identity/src/providers/iam/index.js +7 -7
- package/packages/identity/src/providers/iam/oauth.js +6 -4
- package/packages/identity/src/providers/iam/profile.js +5 -5
- package/packages/identity/tests/auth-config.test.mjs +57 -0
- package/packages/identity/tests/oauth-credentials.test.mjs +13 -2
- package/protocol/FILEMETA_V1.md +95 -0
- package/protocol/INSPECT_VIEWS_V1.md +40 -0
- package/protocol/MERKLE_PERSISTENCE_V1.md +50 -0
- package/protocol/MERKLE_V1.md +3 -1
- package/protocol/capabilities/manifest.json +32 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +41 -0
- package/protocol/context/context-item.schema.json +20 -0
- package/protocol/context/resolved-context-pack.schema.json +42 -0
- package/protocol/context/result-policy.schema.json +17 -0
- package/protocol/knowledge/context-pack.schema.json +33 -0
- package/protocol/knowledge/retrieval-query.schema.json +68 -13
- package/python/README.md +15 -7
- package/python/agentsam_sdk/cli.py +0 -21
- package/python/agentsam_sdk/tui/README.md +17 -12
- package/python/agentsam_sdk/tui/bootstrap.py +2 -2
- package/python/agentsam_sdk/tui/demo.py +25 -10
- package/python/agentsam_sdk/tui/onboarding.py +208 -0
- package/python/tests/test_tui_cli.py +7 -6
- package/skills/README.md +21 -0
- package/skills/agentsam-app-fundamentals/SKILL.md +165 -0
- package/skills/agentsam-app-fundamentals/references/graphs-contracts-ast-merkle.md +89 -0
- package/skills/agentsam-app-fundamentals/references/trust-credentials-and-destinations.md +99 -0
- package/skills/agentsam-jr-dev/SKILL.md +232 -0
- package/skills/agentsam-jr-dev/references/real-application-logic.md +156 -0
- package/skills/agentsam-jr-dev/references/web-application-fundamentals.md +240 -0
- package/skills/agentsam-progression-guard/SKILL.md +197 -0
- package/skills/agentsam-progression-guard/references/checkpoint-chain.md +111 -0
- package/skills/agentsam-progression-guard/references/hooks-operational-io.md +96 -0
- package/skills/catalog.json +53 -0
- package/src/capabilities/index.js +7 -0
- package/src/capabilities/repository-snapshot-view.js +238 -0
- package/src/capabilities/repository-snapshot.js +28 -14
- package/src/cli.js +108 -56
- package/src/commands/cad.js +56 -6
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +17 -31
- package/src/commands/interactive.js +21 -0
- package/src/commands/knowledge.js +27 -4
- package/src/commands/merkle-persist.js +118 -0
- package/src/commands/merkle.js +32 -17
- package/src/commands/models.js +107 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +102 -0
- package/src/commands/product.js +86 -16
- package/src/commands/security.js +3 -3
- package/src/commands/shell.js +71 -27
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/context/budget.js +58 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +5 -0
- package/src/context/resolve.js +84 -0
- package/src/context/result-policy.js +66 -0
- package/src/index.js +16 -0
- package/src/indexing/execution-boundary.js +144 -0
- package/src/indexing/index.js +8 -0
- package/src/indexing/provider.js +41 -0
- package/src/knowledge/config.js +2 -2
- package/src/knowledge/context-pack.js +12 -1
- package/src/knowledge/contracts.js +9 -4
- package/src/knowledge/engine.js +7 -3
- package/src/knowledge/service/server.js +1 -1
- package/src/lib/auth.js +10 -19
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +74 -0
- package/src/lib/core-client.js +8 -8
- package/src/lib/deploy-receipt/index.js +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-status.js +9 -17
- package/src/lib/merkle/cloudflare-persistence.js +321 -0
- package/src/lib/merkle/filemeta.js +43 -0
- package/src/lib/merkle/git-ignore.js +24 -0
- package/src/lib/merkle/hash.js +1 -0
- package/src/lib/merkle/index.js +22 -0
- package/src/lib/merkle/persistence.js +72 -0
- package/src/lib/merkle/semantic.js +359 -0
- package/src/lib/merkle/snapshot.js +9 -3
- package/src/lib/merkle/tree.js +11 -6
- package/src/lib/open-url.js +66 -0
- package/src/lib/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/save-sdk-token.js +1 -1
- package/src/lib/slash-commands.js +2 -1
- package/src/lib/tools.js +11 -5
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/render.js +27 -5
- package/src/security/scan.js +24 -9
- package/src/security/trust-boundary.js +24 -0
- package/src/skills/index.js +64 -0
- package/src/tools/index.js +1 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +56 -0
- package/src/ui/merkle/render.js +1 -0
- package/src/ui/runtime-activity.js +192 -0
- package/src/ui/theme.js +19 -18
- package/test/app-building-skills.test.mjs +61 -0
- package/test/apps-scaffold-contract.test.mjs +56 -0
- package/test/capabilities.test.mjs +53 -4
- package/test/cli-preferences.test.mjs +25 -0
- package/test/context.test.mjs +95 -0
- package/test/indexing-provider.test.mjs +29 -0
- package/test/jr-dev-skill.test.mjs +26 -0
- package/test/knowledge-context-pack.test.mjs +19 -0
- package/test/knowledge.test.mjs +1 -1
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/models.test.mjs +37 -0
- package/test/ollama.test.mjs +94 -0
- package/test/open-url.test.mjs +64 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/runtime-activity.test.mjs +98 -0
- package/test/sdk-worker-contract.test.mjs +68 -0
- package/test/security.test.mjs +46 -0
- package/test/shell.test.mjs +8 -1
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +2 -2
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +27 -0
- package/examples/agentsam-tui-ansi.mjs +0 -149
- package/src/commands/tui.js +0 -120
- package/src/ui/splash-xterm.js +0 -290
- package/src/ui/splash.js +0 -426
|
@@ -0,0 +1,95 @@
|
|
|
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 {
|
|
7
|
+
DEFAULT_RESULT_POLICY,
|
|
8
|
+
createContextBudget,
|
|
9
|
+
normalizeResultPolicy,
|
|
10
|
+
resolveContext,
|
|
11
|
+
resolveProjectContext,
|
|
12
|
+
} from '../src/context/index.js';
|
|
13
|
+
|
|
14
|
+
test('default result policy is bounded and callers may request less', () => {
|
|
15
|
+
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
|
+
});
|
|
21
|
+
assert.throws(() => normalizeResultPolicy({ max_items: 9 }), /higher_detail_required:max_items/);
|
|
22
|
+
assert.throws(() => normalizeResultPolicy({ max_chars: 24_001 }), /higher_detail_required:max_chars/);
|
|
23
|
+
assert.throws(() => normalizeResultPolicy({ detail: 'full' }), /higher_detail_required:detail/);
|
|
24
|
+
assert.equal(normalizeResultPolicy({ detail: 'full', max_items: 20, max_chars: 100_000 }, { operation: 'higher-detail' }).detail, 'full');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('context budget reserves headroom and caps each ownership class', () => {
|
|
28
|
+
const budget = createContextBudget({ windowTokens: 250_000 });
|
|
29
|
+
assert.equal(budget.targetInputTokens, 150_000);
|
|
30
|
+
assert.equal(budget.hardInputTokens, 212_500);
|
|
31
|
+
assert.equal(budget.windowChars, 1_000_000);
|
|
32
|
+
assert.equal(budget.maxSystemChars, 50_000);
|
|
33
|
+
assert.equal(budget.maxToolSchemaChars, 40_000);
|
|
34
|
+
assert.equal(budget.maxEvidenceChars, 100_000);
|
|
35
|
+
assert.equal(budget.maxFileCharsPerRead, 32_768);
|
|
36
|
+
assert.equal(budget.maxFileCharsPerTurn, 131_072);
|
|
37
|
+
assert.equal(budget.maxToolResultChars, 24_000);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test('context.resolve selects high-priority evidence and returns a receipt', () => {
|
|
41
|
+
const budget = createContextBudget({
|
|
42
|
+
windowTokens: 32_000,
|
|
43
|
+
maxEvidenceChars: 90,
|
|
44
|
+
maxFileCharsPerRead: 50,
|
|
45
|
+
maxFileCharsPerTurn: 60,
|
|
46
|
+
maxToolResultChars: 30,
|
|
47
|
+
});
|
|
48
|
+
const pack = resolveContext({
|
|
49
|
+
objective: 'fix terminal cwd persistence',
|
|
50
|
+
refs: ['repo:demo'],
|
|
51
|
+
budget,
|
|
52
|
+
items: [
|
|
53
|
+
{ ref: 'file:low', kind: 'file', priority: 1, content: 'L'.repeat(50) },
|
|
54
|
+
{ ref: 'tool:high', kind: 'tool_result', priority: 10, content: 'T'.repeat(50) },
|
|
55
|
+
{ ref: 'file:high', kind: 'file', priority: 8, content: 'F'.repeat(80) },
|
|
56
|
+
{ ref: 'memory:later', kind: 'memory', priority: 0, content: 'M'.repeat(40) },
|
|
57
|
+
],
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(pack.items.map((item) => item.ref), ['tool:high', 'file:high', 'file:low']);
|
|
61
|
+
assert.equal(pack.items[0].chars, 30);
|
|
62
|
+
assert.equal(pack.items[1].chars, 50);
|
|
63
|
+
assert.equal(pack.items[2].chars, 10);
|
|
64
|
+
assert.equal(pack.receipt.evidence_chars, 90);
|
|
65
|
+
assert.equal(pack.receipt.sources_considered, 4);
|
|
66
|
+
assert.equal(pack.receipt.sources_included, 3);
|
|
67
|
+
assert.ok(pack.receipt.sources_deferred >= 3); // truncated remainders + one deferred source
|
|
68
|
+
assert.ok(pack.receipt.deferred_refs.includes('memory:later'));
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('project context automatically loads bounded .agentsamrules as system context', t => {
|
|
72
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-context-rules-'));
|
|
73
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
74
|
+
fs.writeFileSync(path.join(root, '.agentsamrules'), `# rules\n${'x'.repeat(5000)}\n`);
|
|
75
|
+
const pack = resolveProjectContext({
|
|
76
|
+
cwd: root,
|
|
77
|
+
objective: 'inspect repository',
|
|
78
|
+
budget: createContextBudget({ windowTokens: 8_000, maxSystemChars: 1_000, maxEvidenceChars: 1_000 }),
|
|
79
|
+
items: [],
|
|
80
|
+
});
|
|
81
|
+
assert.equal(pack.rules.found, true);
|
|
82
|
+
assert.equal(pack.rules.chars, 1_000);
|
|
83
|
+
assert.equal(pack.rules.truncated, true);
|
|
84
|
+
assert.equal(pack.receipt.system_chars, 1_000);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test('consumed tool results compact to 4k while preserving the evidence ref', async () => {
|
|
88
|
+
const { compactConsumedToolResult } = await import('../src/context/index.js');
|
|
89
|
+
const item = compactConsumedToolResult('x'.repeat(10_000), { ref: 'tool:call_123', hash: 'sha256:abc' });
|
|
90
|
+
assert.equal(item.ref, 'tool:call_123');
|
|
91
|
+
assert.equal(item.hash, 'sha256:abc');
|
|
92
|
+
assert.equal(item.chars, 4_000);
|
|
93
|
+
assert.equal(item.compacted, true);
|
|
94
|
+
assert.equal(item.source_chars, 10_000);
|
|
95
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import {
|
|
4
|
+
REPOSITORY_KNOWLEDGE_PROVIDER_METHODS,
|
|
5
|
+
assertRepositoryKnowledgeProvider,
|
|
6
|
+
createRepositoryKnowledgeClient,
|
|
7
|
+
describeRepositoryKnowledgeProvider,
|
|
8
|
+
} from '../src/indexing/index.js';
|
|
9
|
+
|
|
10
|
+
function provider() {
|
|
11
|
+
return Object.fromEntries(REPOSITORY_KNOWLEDGE_PROVIDER_METHODS.map((method) => [method, async (input) => ({ method, input })]));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test('repository knowledge provider is a contract, not a bundled platform implementation', async () => {
|
|
15
|
+
const impl = provider();
|
|
16
|
+
assert.equal(assertRepositoryKnowledgeProvider(impl), impl);
|
|
17
|
+
const client = createRepositoryKnowledgeClient(impl);
|
|
18
|
+
assert.deepEqual(await client.findSymbol({ name: 'resolveModel' }), { method: 'findSymbol', input: { name: 'resolveModel' } });
|
|
19
|
+
assert.deepEqual(await client.retrieve({ query: 'session recovery' }), { method: 'retrieve', input: { query: 'session recovery' } });
|
|
20
|
+
assert.throws(() => assertRepositoryKnowledgeProvider({ status() {} }), /refresh\(\) is required/);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('provider capabilities describe implementation truth without prescribing storage', () => {
|
|
24
|
+
assert.deepEqual(describeRepositoryKnowledgeProvider({
|
|
25
|
+
provider: 'inneranimal-platform', structure: 'tree-sitter', lexical: true, semantic: true, graph: true, history: true, evidence: 'merkle',
|
|
26
|
+
}), {
|
|
27
|
+
provider: 'inneranimal-platform', structure: 'tree-sitter', lexical: true, semantic: true, graph: true, history: true, evidence: 'merkle',
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
|
|
6
|
+
const root = path.resolve(import.meta.dirname, '..');
|
|
7
|
+
const skill = path.join(root, 'skills', 'agentsam-jr-dev', 'SKILL.md');
|
|
8
|
+
const fundamentals = path.join(root, 'skills', 'agentsam-jr-dev', 'references', 'web-application-fundamentals.md');
|
|
9
|
+
const logic = path.join(root, 'skills', 'agentsam-jr-dev', 'references', 'real-application-logic.md');
|
|
10
|
+
|
|
11
|
+
test('agentsam-jr-dev ships as a portable skill with depth references', () => {
|
|
12
|
+
for (const file of [skill, fundamentals, logic]) {
|
|
13
|
+
assert.equal(fs.existsSync(file), true, `${file} must exist`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const text = fs.readFileSync(skill, 'utf8');
|
|
17
|
+
assert.match(text, /name: agentsam-jr-dev/);
|
|
18
|
+
assert.match(text, /Inspect before teaching repo-specific facts/);
|
|
19
|
+
assert.match(text, /Real application logic is the standard/);
|
|
20
|
+
assert.match(text, /Never invent a repo architecture/);
|
|
21
|
+
assert.match(text, /frontend owns presentation/);
|
|
22
|
+
assert.match(text, /backend owns trusted execution/);
|
|
23
|
+
|
|
24
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
25
|
+
assert.ok(pkg.files.includes('skills'), 'root npm package must include portable skills');
|
|
26
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { createContextPack } from '../src/knowledge/context-pack.js';
|
|
4
|
+
|
|
5
|
+
test('knowledge ContextPack includes a measurable selection receipt', () => {
|
|
6
|
+
const pack = createContextPack({
|
|
7
|
+
queryId: 'q1',
|
|
8
|
+
query: { text: 'where is auth?', top_k: 2, token_budget: 2000 },
|
|
9
|
+
hits: [{ ref: 'file:a', content: 'abcd' }, { ref: 'file:b', content: '12345678' }],
|
|
10
|
+
diagnostics: { sources_considered: 7, sources_deferred: 5 },
|
|
11
|
+
});
|
|
12
|
+
assert.deepEqual(pack.receipt, {
|
|
13
|
+
chars: 12,
|
|
14
|
+
estimated_tokens: 3,
|
|
15
|
+
sources_considered: 7,
|
|
16
|
+
sources_included: 2,
|
|
17
|
+
sources_deferred: 5,
|
|
18
|
+
});
|
|
19
|
+
});
|
package/test/knowledge.test.mjs
CHANGED
|
@@ -152,5 +152,5 @@ test('recovered KnowledgeClient contracts remain usable with injected transports
|
|
|
152
152
|
const client = new KnowledgeClient({ transport: { async request(op, payload) { operations.push([op, payload]); return { query_id: 'test', hits: [] }; } } });
|
|
153
153
|
await client.retrieve({ text: 'find parser', workspace_id: 'customer' });
|
|
154
154
|
await client.index('repository');
|
|
155
|
-
assert.equal(operations[0][1].top_k,
|
|
155
|
+
assert.equal(operations[0][1].top_k, 8); assert.equal(operations[0][1].token_budget, 6000); assert.equal(operations[0][1].result_policy.max_items, 8); assert.equal(operations[1][0], 'knowledge.index');
|
|
156
156
|
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {
|
|
4
|
+
MERKLE_SNAPSHOT_SCHEMA_SQL,
|
|
5
|
+
MERKLE_SNAPSHOT_STORAGE_PREFIX,
|
|
6
|
+
MERKLE_SNAPSHOT_TABLE,
|
|
7
|
+
MERKLE_WEBSITE_ASSETS_BINDING,
|
|
8
|
+
buildMerklePersistencePlan,
|
|
9
|
+
merklePersistenceUpsertSql,
|
|
10
|
+
merkleSnapshotStorageKey,
|
|
11
|
+
resolveWranglerMerklePersistence,
|
|
12
|
+
} from '../src/lib/merkle/index.js';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import os from 'node:os';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
test('portable Merkle persistence names one table, prefix, and logical asset role', () => {
|
|
18
|
+
assert.equal(MERKLE_SNAPSHOT_TABLE, 'agentsam_fs_merkle_snapshots');
|
|
19
|
+
assert.equal(MERKLE_SNAPSHOT_STORAGE_PREFIX, 'agentsam_fs_merkle_snapshots');
|
|
20
|
+
assert.equal(MERKLE_WEBSITE_ASSETS_BINDING, 'WEBSITE_ASSETS');
|
|
21
|
+
assert.match(MERKLE_SNAPSHOT_SCHEMA_SQL, /metadata_root TEXT/);
|
|
22
|
+
assert.match(MERKLE_SNAPSHOT_SCHEMA_SQL, /classifier_format TEXT/);
|
|
23
|
+
assert.match(MERKLE_SNAPSHOT_SCHEMA_SQL, /storage_bucket TEXT/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('snapshot storage keys stay beneath the canonical prefix and encode repo identity', () => {
|
|
27
|
+
const key = merkleSnapshotStorageKey({
|
|
28
|
+
ownerUserId: 'au_example',
|
|
29
|
+
repoId: 'github:owner/repo',
|
|
30
|
+
snapshotId: 'mrs_example',
|
|
31
|
+
});
|
|
32
|
+
assert.equal(key, 'agentsam_fs_merkle_snapshots/au_example/github%3Aowner%2Frepo/mrs_example.json');
|
|
33
|
+
const escaped = merkleSnapshotStorageKey({ ownerUserId: '../oops', repoId: 'repo', snapshotId: 'snap' });
|
|
34
|
+
assert.ok(!escaped.includes('/../'));
|
|
35
|
+
assert.ok(escaped.includes('..%2Foops'));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('Wrangler persistence resolves logical WEBSITE_ASSETS and DB bindings', () => {
|
|
39
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-wrangler-'));
|
|
40
|
+
const config = path.join(dir, 'wrangler.toml');
|
|
41
|
+
fs.writeFileSync(config, `name = "fixture"\n\n[[d1_databases]]\nbinding = "DB"\ndatabase_name = "fixture-db"\ndatabase_id = "db-id"\n\n[[r2_buckets]]\nbinding = "WEBSITE_ASSETS"\nbucket_name = "fixture-assets"\n`);
|
|
42
|
+
try {
|
|
43
|
+
const resolved = resolveWranglerMerklePersistence({ configPath: config });
|
|
44
|
+
assert.equal(resolved.r2_binding, 'WEBSITE_ASSETS');
|
|
45
|
+
assert.equal(resolved.storage_bucket, 'fixture-assets');
|
|
46
|
+
assert.equal(resolved.d1_binding, 'DB');
|
|
47
|
+
assert.equal(resolved.database_name, 'fixture-db');
|
|
48
|
+
} finally {
|
|
49
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('persistence plan keeps content, policy, and metadata identities separate', () => {
|
|
54
|
+
const snapshot = {
|
|
55
|
+
format: 'agentsam-merkle', version: 1, algorithm: 'sha256',
|
|
56
|
+
rootHash: `sha256:${'1'.repeat(64)}`,
|
|
57
|
+
policyHash: `sha256:${'2'.repeat(64)}`,
|
|
58
|
+
entries: [{ path: 'src/a.js', type: 'file', size: 10, hash: `sha256:${'4'.repeat(64)}` }],
|
|
59
|
+
stats: { files: 1, directories: 1, symlinks: 0, bytes: 10 },
|
|
60
|
+
semantic: {
|
|
61
|
+
format: 'agentsam-filemeta', version: 1, rootHash: `sha256:${'3'.repeat(64)}`,
|
|
62
|
+
classifier: { format: 'agentsam-filemeta', version: 1, source: 'path+package+ast' }, entries: [],
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
const plan = buildMerklePersistencePlan({
|
|
66
|
+
snapshot, root: process.cwd(), ownerUserId: 'au_test', repoId: 'github:owner/repo', source: 'github',
|
|
67
|
+
captureKind: 'agent', connectionId: 'conn_test',
|
|
68
|
+
wrangler: { storage_bucket: 'customer-assets', r2_binding: 'WEBSITE_ASSETS', database_name: 'customer-db', d1_binding: 'DB' },
|
|
69
|
+
});
|
|
70
|
+
assert.equal(plan.row.root_hash, snapshot.rootHash);
|
|
71
|
+
assert.equal(plan.row.policy_hash, snapshot.policyHash);
|
|
72
|
+
assert.equal(plan.row.metadata_root, snapshot.semantic.rootHash);
|
|
73
|
+
assert.equal(plan.row.classifier_format, 'agentsam-filemeta');
|
|
74
|
+
assert.equal(plan.row.storage_bucket, 'customer-assets');
|
|
75
|
+
assert.match(plan.row.storage_key, /^agentsam_fs_merkle_snapshots\//);
|
|
76
|
+
const sql = merklePersistenceUpsertSql(plan.row);
|
|
77
|
+
assert.match(sql, /INSERT INTO agentsam_fs_merkle_snapshots/);
|
|
78
|
+
assert.match(sql, /metadata_root/);
|
|
79
|
+
assert.match(sql, /classifier_source/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('non-deploy persistence requires execution provenance', () => {
|
|
83
|
+
const snapshot = {
|
|
84
|
+
format: 'agentsam-merkle', version: 1, algorithm: 'sha256', rootHash: `sha256:${'1'.repeat(64)}`,
|
|
85
|
+
policyHash: `sha256:${'2'.repeat(64)}`, entries: [], stats: { files: 0, directories: 0, symlinks: 0, bytes: 0 },
|
|
86
|
+
};
|
|
87
|
+
assert.throws(() => buildMerklePersistencePlan({
|
|
88
|
+
snapshot, root: process.cwd(), ownerUserId: 'au_test', repoId: 'github:owner/repo', captureKind: 'agent',
|
|
89
|
+
wrangler: { storage_bucket: 'customer-assets', r2_binding: 'WEBSITE_ASSETS', database_name: 'customer-db', d1_binding: 'DB' },
|
|
90
|
+
}), /execution_provenance_required/);
|
|
91
|
+
});
|
package/test/merkle.test.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import os from 'node:os';
|
|
|
5
5
|
import path from 'node:path';
|
|
6
6
|
import { spawnSync } from 'node:child_process';
|
|
7
7
|
import { fileURLToPath } from 'node:url';
|
|
8
|
-
import { buildMerkleTree, saveSnapshot, readSnapshot, validateSnapshot, diffTrees } from '../src/lib/merkle/index.js';
|
|
8
|
+
import { buildMerkleTree, saveSnapshot, readSnapshot, validateSnapshot, diffTrees, metadataRoot } from '../src/lib/merkle/index.js';
|
|
9
9
|
|
|
10
10
|
const cli = fileURLToPath(new URL('../src/cli.js', import.meta.url));
|
|
11
11
|
async function fixture(t) {
|
|
@@ -60,6 +60,55 @@ test('default ignores, explicit dist inclusion, literal exclusions, and empty-di
|
|
|
60
60
|
assert.equal(excluded.stats.files, 0);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
test('semantic metadata adds package/system/category/mode and AST evidence without changing the content root', async (t) => {
|
|
64
|
+
const root = await fixture(t);
|
|
65
|
+
await write(root, 'package.json', JSON.stringify({ name: 'fixture-root', version: '1.0.0' }));
|
|
66
|
+
await write(root, 'packages/identity/package.json', JSON.stringify({
|
|
67
|
+
name: '@example/identity',
|
|
68
|
+
version: '1.0.0',
|
|
69
|
+
agentsam: { system: 'identity', kind: 'library', tags: ['authentication'] },
|
|
70
|
+
}));
|
|
71
|
+
await write(root, 'packages/identity/src/core/accounts.js', "import { helper } from '../shared/helper.js';\nexport function resolveAccount() { return helper; }\n");
|
|
72
|
+
await write(root, 'packages/identity/src/shared/helper.js', 'export const helper = 1;\n');
|
|
73
|
+
await fs.chmod(path.join(root, 'packages/identity/src/core/accounts.js'), 0o644);
|
|
74
|
+
|
|
75
|
+
const plain = await buildMerkleTree(root);
|
|
76
|
+
const enriched = await buildMerkleTree(root, { semantic: true });
|
|
77
|
+
assert.equal(enriched.rootHash, plain.rootHash);
|
|
78
|
+
assert.match(enriched.policyHash, /^sha256:[a-f0-9]{64}$/);
|
|
79
|
+
assert.equal(enriched.policyHash, plain.policyHash);
|
|
80
|
+
assert.equal(enriched.version, 1);
|
|
81
|
+
assert.match(enriched.semantic.rootHash, /^sha256:[a-f0-9]{64}$/);
|
|
82
|
+
assert.equal(enriched.semantic.classifier.format, 'agentsam-filemeta');
|
|
83
|
+
assert.equal(enriched.semantic.classifier.version, 1);
|
|
84
|
+
|
|
85
|
+
const accounts = enriched.semantic.entries.find((entry) => entry.path === 'packages/identity/src/core/accounts.js');
|
|
86
|
+
assert.equal(accounts.mode, 420);
|
|
87
|
+
assert.equal(accounts.package, '@example/identity');
|
|
88
|
+
assert.equal(accounts.package_root, 'packages/identity');
|
|
89
|
+
assert.equal(accounts.package_kind, 'library');
|
|
90
|
+
assert.equal(accounts.system, 'identity');
|
|
91
|
+
assert.equal(accounts.category, 'accounts');
|
|
92
|
+
assert.equal(accounts.layer, 'core');
|
|
93
|
+
assert.equal(accounts.kind, 'source');
|
|
94
|
+
assert.equal(accounts.language, 'javascript');
|
|
95
|
+
assert.equal(accounts.role, 'business-logic');
|
|
96
|
+
assert.deepEqual(accounts.tags, ['authentication']);
|
|
97
|
+
assert.deepEqual(accounts.symbols, ['resolveAccount']);
|
|
98
|
+
assert.deepEqual(accounts.imports, ['../shared/helper.js']);
|
|
99
|
+
assert.equal(accounts.ast.indexed, true);
|
|
100
|
+
|
|
101
|
+
const changedClassifier = { ...enriched.semantic.classifier, version: 2 };
|
|
102
|
+
assert.notEqual(metadataRoot(enriched.semantic.entries, changedClassifier), enriched.semantic.rootHash);
|
|
103
|
+
|
|
104
|
+
await fs.chmod(path.join(root, 'packages/identity/src/core/accounts.js'), 0o755);
|
|
105
|
+
const permissionOnly = await buildMerkleTree(root, { semantic: true });
|
|
106
|
+
const permissionRecord = permissionOnly.semantic.entries.find((entry) => entry.path === 'packages/identity/src/core/accounts.js');
|
|
107
|
+
assert.equal(permissionOnly.rootHash, plain.rootHash);
|
|
108
|
+
assert.equal(permissionRecord.mode, 493);
|
|
109
|
+
assert.notEqual(permissionOnly.semantic.rootHash, enriched.semantic.rootHash);
|
|
110
|
+
});
|
|
111
|
+
|
|
63
112
|
test('symlinks hash their literal target, including cycles, without reading the target', { skip: process.platform === 'win32' }, async (t) => {
|
|
64
113
|
const root = await fixture(t), outside = await fixture(t);
|
|
65
114
|
await write(outside, 'secret.txt', 'not read');
|
|
@@ -78,10 +127,14 @@ test('default and custom snapshots exclude themselves and preserve baseline unti
|
|
|
78
127
|
const root = await fixture(t);
|
|
79
128
|
await write(root, 'file.txt', 'baseline');
|
|
80
129
|
const initial = await buildMerkleTree(root);
|
|
81
|
-
const { snapshot, output } = await saveSnapshot(root);
|
|
130
|
+
const { snapshot, output } = await saveSnapshot(root, { semantic: true });
|
|
82
131
|
assert.equal(initial.rootHash, snapshot.rootHash);
|
|
132
|
+
assert.match(snapshot.semantic.rootHash, /^sha256:[a-f0-9]{64}$/);
|
|
83
133
|
assert.ok(diffTrees(snapshot, await buildMerkleTree(root, { policy: snapshot.policy })).equal);
|
|
84
|
-
|
|
134
|
+
const reloaded = await readSnapshot(output);
|
|
135
|
+
assert.equal(reloaded.rootHash, snapshot.rootHash);
|
|
136
|
+
assert.equal(reloaded.policyHash, snapshot.policyHash);
|
|
137
|
+
assert.equal(reloaded.semantic.rootHash, snapshot.semantic.rootHash);
|
|
85
138
|
await assert.rejects(saveSnapshot(root), /exists/);
|
|
86
139
|
await write(root, 'file.txt', 'edited');
|
|
87
140
|
assert.equal(diffTrees(snapshot, await buildMerkleTree(root, { policy: snapshot.policy })).stats.modified, 1);
|
|
@@ -142,3 +195,24 @@ test('CLI snapshot/verify/diff supports JSON, moved roots, and distinct mismatch
|
|
|
142
195
|
assert.equal(piped.status, 0, piped.stderr);
|
|
143
196
|
assert.ok(!piped.stdout.includes('\x1b'));
|
|
144
197
|
});
|
|
198
|
+
|
|
199
|
+
test('semantic index records execution domains, environment access, and resolved local imports as Merkle-bound evidence', async (t) => {
|
|
200
|
+
const root = await fixture(t);
|
|
201
|
+
await write(root, 'package.json', JSON.stringify({ name: 'boundary-fixture', version: '1.0.0' }));
|
|
202
|
+
await write(root, 'frontend/client.ts', "import { contract } from '../shared/contract.ts';\nexport const api = import.meta.env.VITE_API_URL + contract;\n");
|
|
203
|
+
await write(root, 'shared/contract.ts', "export const contract = 'v1';\n");
|
|
204
|
+
await write(root, 'backend/private.ts', "export const key = process.env.API_KEY;\n");
|
|
205
|
+
const tree = await buildMerkleTree(root, { semantic: true });
|
|
206
|
+
const client = tree.semantic.entries.find((entry) => entry.path === 'frontend/client.ts');
|
|
207
|
+
const shared = tree.semantic.entries.find((entry) => entry.path === 'shared/contract.ts');
|
|
208
|
+
const backend = tree.semantic.entries.find((entry) => entry.path === 'backend/private.ts');
|
|
209
|
+
assert.equal(client.execution_domain, 'browser');
|
|
210
|
+
assert.equal(shared.execution_domain, 'shared');
|
|
211
|
+
assert.equal(backend.execution_domain, 'server');
|
|
212
|
+
assert.deepEqual(client.env_accesses, [{ source: 'import.meta.env', name: 'VITE_API_URL' }]);
|
|
213
|
+
assert.deepEqual(client.resolved_imports, [{ specifier: '../shared/contract.ts', target: 'shared/contract.ts' }]);
|
|
214
|
+
assert.deepEqual(backend.env_accesses, [{ source: 'process.env', name: 'API_KEY' }]);
|
|
215
|
+
assert.equal(tree.semantic.stats.by_execution_domain.browser, 1);
|
|
216
|
+
assert.equal(tree.semantic.stats.by_execution_domain.server, 1);
|
|
217
|
+
assert.equal(tree.semantic.classifier.trust_boundary, 'execution-domain-v1');
|
|
218
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { collectModelsStatus, renderModelsStatus } from '../src/commands/models.js';
|
|
5
|
+
|
|
6
|
+
function response(body, status = 200) {
|
|
7
|
+
return {
|
|
8
|
+
ok: status >= 200 && status < 300,
|
|
9
|
+
status,
|
|
10
|
+
async json() { return body; },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
test('model inventory reports configured API providers without exposing credentials', async () => {
|
|
15
|
+
const status = await collectModelsStatus({
|
|
16
|
+
env: {
|
|
17
|
+
OPENAI_API_KEY: 'secret-openai',
|
|
18
|
+
GEMINI_API_KEY: '',
|
|
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',
|
|
23
|
+
},
|
|
24
|
+
fetchImpl: async () => response({ models: [{ name: 'qwen:test' }, { name: 'embed:test' }] }),
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
assert.equal(status.providers.find((row) => row.id === 'openai').configured, true);
|
|
28
|
+
assert.equal(status.providers.find((row) => row.id === 'gemini').configured, false);
|
|
29
|
+
assert.equal(status.providers.find((row) => row.id === 'grok').configured, true);
|
|
30
|
+
assert.equal(status.local.online, true);
|
|
31
|
+
assert.deepEqual(status.local.models.map((row) => row.name), ['qwen:test', 'embed:test']);
|
|
32
|
+
|
|
33
|
+
const rendered = renderModelsStatus(status);
|
|
34
|
+
assert.match(rendered, /OpenAI/);
|
|
35
|
+
assert.match(rendered, /qwen:test/);
|
|
36
|
+
assert.doesNotMatch(rendered, /secret-openai|secret-xai/);
|
|
37
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
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 {
|
|
7
|
+
OLLAMA_DEFAULTS,
|
|
8
|
+
ollamaInstallPlan,
|
|
9
|
+
parseOllamaArgs,
|
|
10
|
+
probeOllama,
|
|
11
|
+
resolveOllamaConfig,
|
|
12
|
+
updateProjectOllamaConfig,
|
|
13
|
+
upsertOllamaEnvFile,
|
|
14
|
+
} from '../src/commands/ollama.js';
|
|
15
|
+
import { createProjectManifest } from '../src/lib/project-config.js';
|
|
16
|
+
|
|
17
|
+
test('Ollama defaults are local-only and match the SDK local-dev contract', () => {
|
|
18
|
+
assert.deepEqual(OLLAMA_DEFAULTS, {
|
|
19
|
+
baseUrl: 'http://127.0.0.1:11434',
|
|
20
|
+
model: 'qwen2.5-coder',
|
|
21
|
+
embedModel: 'mxbai-embed-large',
|
|
22
|
+
});
|
|
23
|
+
assert.deepEqual(resolveOllamaConfig({}, {}), OLLAMA_DEFAULTS);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('Ollama setup args require explicit install/pull/start opt-ins', () => {
|
|
27
|
+
assert.deepEqual(parseOllamaArgs(['setup']), {
|
|
28
|
+
command: 'setup',
|
|
29
|
+
cwd: process.cwd(),
|
|
30
|
+
envFile: '',
|
|
31
|
+
baseUrl: '',
|
|
32
|
+
model: '',
|
|
33
|
+
embedModel: '',
|
|
34
|
+
install: false,
|
|
35
|
+
start: false,
|
|
36
|
+
pull: false,
|
|
37
|
+
json: false,
|
|
38
|
+
});
|
|
39
|
+
const opted = parseOllamaArgs(['setup', '--install', '--start', '--pull', '--json']);
|
|
40
|
+
assert.equal(opted.install, true);
|
|
41
|
+
assert.equal(opted.start, true);
|
|
42
|
+
assert.equal(opted.pull, true);
|
|
43
|
+
assert.equal(opted.json, true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('setup env writer updates only canonical OLLAMA keys and preserves other config', () => {
|
|
47
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-ollama-'));
|
|
48
|
+
const envFile = path.join(root, '.env');
|
|
49
|
+
fs.writeFileSync(envFile, 'EXISTING=value\nOLLAMA_MODEL=old\n');
|
|
50
|
+
upsertOllamaEnvFile(envFile, OLLAMA_DEFAULTS);
|
|
51
|
+
const text = fs.readFileSync(envFile, 'utf8');
|
|
52
|
+
assert.match(text, /^EXISTING=value$/m);
|
|
53
|
+
assert.match(text, /^OLLAMA_BASE_URL=http:\/\/127\.0\.0\.1:11434$/m);
|
|
54
|
+
assert.match(text, /^OLLAMA_MODEL=qwen2\.5-coder$/m);
|
|
55
|
+
assert.match(text, /^OLLAMA_EMBED_MODEL=mxbai-embed-large$/m);
|
|
56
|
+
assert.equal((text.match(/^OLLAMA_MODEL=/gm) || []).length, 1);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test('project config records only portable Ollama capability metadata', () => {
|
|
60
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-ollama-config-'));
|
|
61
|
+
fs.mkdirSync(path.join(root, '.agentsam'));
|
|
62
|
+
fs.writeFileSync(path.join(root, '.agentsam', 'config.json'), `${JSON.stringify(createProjectManifest({ projectName: 'demo', repositoryId: 'local:demo' }), null, 2)}\n`);
|
|
63
|
+
const filename = updateProjectOllamaConfig(root, OLLAMA_DEFAULTS);
|
|
64
|
+
const config = JSON.parse(fs.readFileSync(filename, 'utf8'));
|
|
65
|
+
assert.equal(config.models.local.provider, 'ollama');
|
|
66
|
+
assert.equal(config.models.local.base_url_env, 'OLLAMA_BASE_URL');
|
|
67
|
+
assert.equal(config.models.local.model_env, 'OLLAMA_MODEL');
|
|
68
|
+
assert.equal(config.models.local.embed_model_env, 'OLLAMA_EMBED_MODEL');
|
|
69
|
+
assert.equal(JSON.stringify(config).includes('127.0.0.1'), false);
|
|
70
|
+
assert.equal(JSON.stringify(config).includes('qwen2.5-coder'), false);
|
|
71
|
+
assert.equal(JSON.stringify(config).includes('token'), false);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('status probe recognizes configured chat and embedding models', async () => {
|
|
75
|
+
const fakeFetch = async () => new Response(JSON.stringify({ models: [
|
|
76
|
+
{ name: 'qwen2.5-coder:latest' },
|
|
77
|
+
{ name: 'mxbai-embed-large:latest' },
|
|
78
|
+
] }), { status: 200, headers: { 'content-type': 'application/json' } });
|
|
79
|
+
const status = await probeOllama(OLLAMA_DEFAULTS, fakeFetch);
|
|
80
|
+
assert.equal(status.online, true);
|
|
81
|
+
assert.equal(status.chat_ready, true);
|
|
82
|
+
assert.equal(status.embed_ready, true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test('automatic install plans use local package managers, never a remote shell script', () => {
|
|
86
|
+
assert.deepEqual(ollamaInstallPlan('darwin', { brew: true }), {
|
|
87
|
+
command: 'brew', args: ['install', 'ollama'], manager: 'homebrew',
|
|
88
|
+
});
|
|
89
|
+
assert.equal(ollamaInstallPlan('linux', { brew: false }), null);
|
|
90
|
+
assert.equal(ollamaInstallPlan('darwin', { brew: false }), null);
|
|
91
|
+
const win = ollamaInstallPlan('win32', { winget: true });
|
|
92
|
+
assert.equal(win.command, 'winget');
|
|
93
|
+
assert.equal(win.args.includes('Ollama.Ollama'), true);
|
|
94
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { PassThrough } from 'node:stream';
|
|
4
|
+
import { browserCommand, promptToOpenUrl } from '../src/lib/open-url.js';
|
|
5
|
+
|
|
6
|
+
test('browserCommand uses argv-safe platform launchers', () => {
|
|
7
|
+
assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'darwin'), {
|
|
8
|
+
command: 'open',
|
|
9
|
+
args: ['https://inneranimalmedia.com/auth'],
|
|
10
|
+
});
|
|
11
|
+
assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'linux'), {
|
|
12
|
+
command: 'xdg-open',
|
|
13
|
+
args: ['https://inneranimalmedia.com/auth'],
|
|
14
|
+
});
|
|
15
|
+
assert.deepEqual(browserCommand('https://inneranimalmedia.com/auth', 'win32'), {
|
|
16
|
+
command: 'cmd',
|
|
17
|
+
args: ['/c', 'start', '', 'https://inneranimalmedia.com/auth'],
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('browserCommand rejects non-http protocols', () => {
|
|
22
|
+
assert.throws(() => browserCommand('file:///tmp/example', 'darwin'), /Unsupported URL protocol/);
|
|
23
|
+
assert.throws(() => browserCommand('javascript:alert(1)', 'darwin'), /Unsupported URL protocol/);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('promptToOpenUrl stays non-interactive when stdin is not a TTY', async () => {
|
|
27
|
+
const input = new PassThrough();
|
|
28
|
+
const output = new PassThrough();
|
|
29
|
+
let text = '';
|
|
30
|
+
output.on('data', chunk => { text += chunk.toString(); });
|
|
31
|
+
let opened = false;
|
|
32
|
+
const result = await promptToOpenUrl('https://www.blender.org/download/', {
|
|
33
|
+
input,
|
|
34
|
+
output,
|
|
35
|
+
openImpl: () => { opened = true; },
|
|
36
|
+
});
|
|
37
|
+
assert.equal(result.interactive, false);
|
|
38
|
+
assert.equal(result.opened, false);
|
|
39
|
+
assert.equal(opened, false);
|
|
40
|
+
assert.match(text, /https:\/\/www\.blender\.org\/download\//);
|
|
41
|
+
assert.match(text, /Open the URL above in a browser/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('promptToOpenUrl waits for Enter before opening in a TTY', async () => {
|
|
45
|
+
const input = new PassThrough();
|
|
46
|
+
const output = new PassThrough();
|
|
47
|
+
input.isTTY = true;
|
|
48
|
+
output.isTTY = true;
|
|
49
|
+
let text = '';
|
|
50
|
+
output.on('data', chunk => { text += chunk.toString(); });
|
|
51
|
+
let openedUrl = null;
|
|
52
|
+
const pending = promptToOpenUrl('https://inneranimalmedia.com/dashboard', {
|
|
53
|
+
input,
|
|
54
|
+
output,
|
|
55
|
+
openImpl: url => { openedUrl = url; },
|
|
56
|
+
});
|
|
57
|
+
input.write('\n');
|
|
58
|
+
const result = await pending;
|
|
59
|
+
assert.equal(result.interactive, true);
|
|
60
|
+
assert.equal(result.opened, true);
|
|
61
|
+
assert.equal(openedUrl, 'https://inneranimalmedia.com/dashboard');
|
|
62
|
+
assert.match(text, /Press ENTER to open/);
|
|
63
|
+
assert.match(text, /Browser opened/);
|
|
64
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
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 { execFileSync } from 'node:child_process';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import {
|
|
8
|
+
createProjectManifest,
|
|
9
|
+
getDefaultProfile,
|
|
10
|
+
getDefaultRuntime,
|
|
11
|
+
getDeployTarget,
|
|
12
|
+
getLocalDatabasePath,
|
|
13
|
+
getProjectName,
|
|
14
|
+
getProjectPreset,
|
|
15
|
+
getProjectRulesPath,
|
|
16
|
+
getRepositoryId,
|
|
17
|
+
portableRepositoryIdFromGit,
|
|
18
|
+
} from '../src/lib/project-config.js';
|
|
19
|
+
|
|
20
|
+
function git(cwd, args) {
|
|
21
|
+
return execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('portable project manifest is small and excludes runtime/account history', () => {
|
|
25
|
+
const config = createProjectManifest({
|
|
26
|
+
projectName: 'demo',
|
|
27
|
+
preset: 'cms',
|
|
28
|
+
profile: 'cms',
|
|
29
|
+
runTarget: 'local',
|
|
30
|
+
sdkVersion: '2.5.0',
|
|
31
|
+
repositoryId: 'github:owner/demo',
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
assert.equal(config.schema_version, 2);
|
|
35
|
+
assert.deepEqual(config.project, { name: 'demo' });
|
|
36
|
+
assert.equal(config.repository.id, 'github:owner/demo');
|
|
37
|
+
assert.equal(config.product.preset, 'cms');
|
|
38
|
+
assert.equal(config.defaults.profile, 'cms');
|
|
39
|
+
assert.equal(config.defaults.runtime, 'local');
|
|
40
|
+
assert.equal(config.defaults.model, 'auto');
|
|
41
|
+
assert.deepEqual(config.merkle, { enabled: true, semantic: true, persistence: 'auto' });
|
|
42
|
+
assert.equal(config.rules.file, '.agentsamrules');
|
|
43
|
+
assert.equal(getProjectRulesPath(config), '.agentsamrules');
|
|
44
|
+
assert.equal(config.knowledge.config, '.agentsam/knowledge.json');
|
|
45
|
+
assert.equal(config.local.database, '.agentsam/data/agentsam.sqlite');
|
|
46
|
+
|
|
47
|
+
const forbidden = [
|
|
48
|
+
'account_id', 'user_id', 'tenant_id', 'workspace_id', 'current_run_id', 'plan_id', 'task_id',
|
|
49
|
+
'connection_id', 'runtime_lease_id', 'latest_merkle_root', 'root_hash', 'model_history',
|
|
50
|
+
'subagent_runs', 'pty_port', 'dev_port', 'deployed_at', 'cloudflare',
|
|
51
|
+
];
|
|
52
|
+
const serialized = JSON.stringify(config);
|
|
53
|
+
for (const field of forbidden) assert.equal(serialized.includes(`"${field}"`), false, field);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('project config accessors keep legacy flat projects readable during migration', () => {
|
|
57
|
+
const legacy = {
|
|
58
|
+
project: 'legacy-demo',
|
|
59
|
+
lane: 'data',
|
|
60
|
+
agent: 'data',
|
|
61
|
+
run_target: 'local',
|
|
62
|
+
deploy_target: 'gcp',
|
|
63
|
+
db_path: '.agentsam/old.sqlite',
|
|
64
|
+
};
|
|
65
|
+
assert.equal(getProjectName(legacy), 'legacy-demo');
|
|
66
|
+
assert.equal(getProjectPreset(legacy), 'data');
|
|
67
|
+
assert.equal(getDefaultProfile(legacy), 'data');
|
|
68
|
+
assert.equal(getDefaultRuntime(legacy), 'local');
|
|
69
|
+
assert.equal(getDeployTarget(legacy), 'gcp');
|
|
70
|
+
assert.equal(getLocalDatabasePath(legacy), '.agentsam/old.sqlite');
|
|
71
|
+
assert.equal(getRepositoryId(legacy), null);
|
|
72
|
+
assert.equal(getProjectRulesPath(legacy), '.agentsamrules');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('GitHub remote becomes portable repository identity when adopting an existing repo', t => {
|
|
76
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-project-config-'));
|
|
77
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
78
|
+
git(root, ['init', '-q']);
|
|
79
|
+
git(root, ['remote', 'add', 'origin', 'git@github.com:ExampleOrg/DemoRepo.git']);
|
|
80
|
+
assert.equal(portableRepositoryIdFromGit(root), 'github:exampleorg/demorepo');
|
|
81
|
+
});
|