@inneranimalmedia/agentsam-sdk 2.4.1 → 2.6.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/AGENTSAM.md +55 -0
- package/README.md +17 -12
- package/bin/agentsam +2 -0
- package/docs/AGENTSAM_ASTRA_OPENAI_INTEGRATION.md +1363 -0
- 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 +167 -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 +35 -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 +35 -10
- 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 +10 -1
- package/packages/identity/src/contracts/auth-config.js +109 -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 +61 -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/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 +79 -3
- package/protocol/capabilities/repository-snapshot.schema.json +1 -0
- package/protocol/context/context-budget.schema.json +36 -0
- package/protocol/context/context-item.schema.json +19 -0
- package/protocol/context/resolved-context-pack.schema.json +47 -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/protocol/models/README.md +373 -0
- package/protocol/models/model-inventory-v2.schema.json +212 -0
- 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-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/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 +71 -0
- package/src/agent/capability-adapter.js +25 -13
- package/src/agent/index.js +1 -0
- package/src/agent/responses-runner.js +325 -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 +198 -76
- 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 +114 -0
- package/src/commands/context.js +14 -2
- package/src/commands/db.js +4 -7
- package/src/commands/deploy.js +54 -32
- package/src/commands/eval.js +63 -0
- package/src/commands/interactive.js +18 -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 +152 -0
- package/src/commands/ollama.js +259 -0
- package/src/commands/preferences.js +144 -0
- package/src/commands/product.js +86 -16
- package/src/commands/resume.js +67 -0
- package/src/commands/security.js +8 -6
- package/src/commands/shell.js +405 -100
- package/src/commands/skills.js +66 -0
- package/src/commands/start-local.js +1 -1
- package/src/commands/tunnel.js +5 -4
- package/src/commands/whoami.js +86 -0
- package/src/context/budget.js +120 -0
- package/src/context/compact.js +28 -0
- package/src/context/index.js +7 -0
- package/src/context/rehydrate.js +35 -0
- package/src/context/resolve.js +116 -0
- package/src/context/result-policy.js +66 -0
- package/src/errors/diagnostic.js +160 -0
- package/src/errors/index.js +9 -0
- package/src/eval/context.js +191 -0
- package/src/eval/index.js +1 -0
- package/src/index.js +70 -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/account-session.js +98 -0
- package/src/lib/agent-instructions.js +73 -0
- package/src/lib/auth.js +4 -0
- package/src/lib/bridge-client.js +7 -5
- package/src/lib/cli-preferences.js +78 -0
- package/src/lib/core-client.js +8 -8
- 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 +5 -2
- package/src/lib/detect-context.js +6 -5
- package/src/lib/execution-approvals.js +59 -0
- package/src/lib/identity-scaffold.js +1 -1
- package/src/lib/local-scaffold.js +35 -33
- package/src/lib/local-sessions.js +127 -0
- 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/project-config.js +227 -0
- package/src/lib/project-rules.js +68 -0
- package/src/lib/provider-credentials.js +83 -0
- package/src/lib/save-sdk-token.js +1 -1
- 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 +22 -15
- package/src/lib/tools.js +11 -5
- package/src/models/catalog.js +135 -0
- package/src/models/index.js +7 -0
- package/src/providers/index.js +5 -0
- package/src/providers/openai-responses.js +275 -0
- package/src/security/index.js +1 -0
- package/src/security/inventory.js +4 -1
- package/src/security/process.js +35 -9
- 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/telemetry/contracts.js +203 -0
- package/src/telemetry/events.js +48 -0
- package/src/telemetry/index.js +8 -0
- package/src/tools/hydrate.js +35 -0
- package/src/tools/index.js +2 -0
- package/src/tools/search.js +70 -0
- package/src/ui/ansi.js +1 -1
- package/src/ui/boot.js +54 -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/account-session.test.mjs +36 -0
- 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 +46 -0
- package/test/cloudflare-connector.test.mjs +96 -0
- package/test/cloudflare-runtime.test.mjs +75 -0
- package/test/context.test.mjs +144 -0
- package/test/deploy-health-scan.test.mjs +67 -0
- package/test/error-diagnostics.test.mjs +59 -0
- package/test/eval-context.test.mjs +37 -0
- package/test/execution-approvals.test.mjs +27 -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/local-sessions.test.mjs +42 -0
- package/test/local-studio-deploy.test.mjs +83 -0
- package/test/merkle-persistence.test.mjs +91 -0
- package/test/merkle.test.mjs +77 -3
- package/test/model-catalog.test.mjs +43 -0
- package/test/models.test.mjs +51 -0
- package/test/npm10-lock.test.mjs +29 -0
- package/test/ollama.test.mjs +94 -0
- package/test/openai-responses.test.mjs +95 -0
- package/test/project-config.test.mjs +81 -0
- package/test/project-rules.test.mjs +44 -0
- package/test/provider-credentials.test.mjs +52 -0
- package/test/rehydrate.test.mjs +25 -0
- package/test/release-hygiene.test.mjs +34 -0
- package/test/repository-snapshot-view.test.mjs +112 -0
- package/test/responses-runner.test.mjs +148 -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 +55 -21
- package/test/skills.test.mjs +22 -0
- package/test/smoke.mjs +5 -2
- package/test/telemetry.test.mjs +79 -0
- package/test/theme-portability.test.mjs +14 -0
- package/test/tools-search.test.mjs +40 -0
- package/test/whoami-resume.test.mjs +56 -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,144 @@
|
|
|
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
|
+
assessContextUsage,
|
|
9
|
+
compileAgentInstructions,
|
|
10
|
+
createContextBudget,
|
|
11
|
+
normalizeResultPolicy,
|
|
12
|
+
resolveContext,
|
|
13
|
+
resolveProjectContext,
|
|
14
|
+
} from '../src/context/index.js';
|
|
15
|
+
|
|
16
|
+
test('default result policy is bounded and callers may request less', () => {
|
|
17
|
+
assert.deepEqual(DEFAULT_RESULT_POLICY, { max_items: 8, max_chars: 24_000, detail: 'excerpt' });
|
|
18
|
+
assert.deepEqual(normalizeResultPolicy({ max_items: 3, max_chars: 8_000, detail: 'card' }), { max_items: 3, max_chars: 8_000, detail: 'card' });
|
|
19
|
+
assert.throws(() => normalizeResultPolicy({ max_items: 9 }), /higher_detail_required:max_items/);
|
|
20
|
+
assert.throws(() => normalizeResultPolicy({ max_chars: 24_001 }), /higher_detail_required:max_chars/);
|
|
21
|
+
assert.throws(() => normalizeResultPolicy({ detail: 'full' }), /higher_detail_required:detail/);
|
|
22
|
+
assert.equal(normalizeResultPolicy({ detail: 'full', max_items: 20, max_chars: 100_000 }, { operation: 'higher-detail' }).detail, 'full');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('legacy ratio context budget remains compatible for ordinary windows', () => {
|
|
26
|
+
const budget = createContextBudget({ windowTokens: 250_000 });
|
|
27
|
+
assert.equal(budget.targetInputTokens, 150_000);
|
|
28
|
+
assert.equal(budget.hardInputTokens, 212_500);
|
|
29
|
+
assert.equal(budget.windowChars, 1_000_000);
|
|
30
|
+
assert.equal(budget.maxSystemChars, 50_000);
|
|
31
|
+
assert.equal(budget.maxToolSchemaChars, 40_000);
|
|
32
|
+
assert.equal(budget.maxEvidenceChars, 100_000);
|
|
33
|
+
assert.equal(budget.maxFileCharsPerRead, 32_768);
|
|
34
|
+
assert.equal(budget.maxFileCharsPerTurn, 131_072);
|
|
35
|
+
assert.equal(budget.maxToolResultChars, 24_000);
|
|
36
|
+
});
|
|
37
|
+
|
|
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', () => {
|
|
65
|
+
const budget = createContextBudget({
|
|
66
|
+
windowTokens: 32_000,
|
|
67
|
+
maxEvidenceChars: 90,
|
|
68
|
+
maxFileCharsPerRead: 50,
|
|
69
|
+
maxFileCharsPerTurn: 60,
|
|
70
|
+
maxToolResultChars: 30,
|
|
71
|
+
});
|
|
72
|
+
const pack = resolveContext({
|
|
73
|
+
objective: 'fix terminal cwd persistence',
|
|
74
|
+
refs: ['repo:demo'],
|
|
75
|
+
budget,
|
|
76
|
+
instructionChars: 20,
|
|
77
|
+
toolSchemaChars: 12,
|
|
78
|
+
historyChars: 8,
|
|
79
|
+
items: [
|
|
80
|
+
{ ref: 'file:low', kind: 'file', priority: 1, content: 'L'.repeat(50) },
|
|
81
|
+
{ ref: 'tool:high', kind: 'tool_result', priority: 10, content: 'T'.repeat(50) },
|
|
82
|
+
{ ref: 'file:high', kind: 'file', priority: 8, content: 'F'.repeat(80) },
|
|
83
|
+
{ ref: 'memory:later', kind: 'memory', priority: 0, content: 'M'.repeat(40) },
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
assert.deepEqual(pack.items.map((item) => item.ref), ['tool:high', 'file:high', 'file:low']);
|
|
88
|
+
assert.equal(pack.items[0].chars, 30);
|
|
89
|
+
assert.equal(pack.items[1].chars, 50);
|
|
90
|
+
assert.equal(pack.items[2].chars, 10);
|
|
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);
|
|
99
|
+
assert.equal(pack.receipt.sources_considered, 4);
|
|
100
|
+
assert.equal(pack.receipt.sources_included, 3);
|
|
101
|
+
assert.ok(pack.receipt.sources_deferred >= 3);
|
|
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'));
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('project context loads bounded compiled AgentSam instructions', t => {
|
|
119
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'agentsam-context-rules-'));
|
|
120
|
+
t.after(() => fs.rmSync(root, { recursive: true, force: true }));
|
|
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');
|
|
124
|
+
const pack = resolveProjectContext({
|
|
125
|
+
cwd: root,
|
|
126
|
+
objective: 'inspect repository',
|
|
127
|
+
budget: createContextBudget({ windowTokens: 8_000, maxSystemChars: 1_000, maxEvidenceChars: 1_000 }),
|
|
128
|
+
items: [],
|
|
129
|
+
});
|
|
130
|
+
assert.equal(pack.rules.found, true);
|
|
131
|
+
assert.equal(pack.rules.chars, 1_000);
|
|
132
|
+
assert.equal(pack.rules.truncated, true);
|
|
133
|
+
assert.equal(pack.receipt.system_chars, 1_000);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test('consumed tool results compact to 4k while preserving rehydration identity', async () => {
|
|
137
|
+
const { compactConsumedToolResult } = await import('../src/context/index.js');
|
|
138
|
+
const item = compactConsumedToolResult('x'.repeat(10_000), { ref: 'tool:call_123', hash: 'sha256:abc' });
|
|
139
|
+
assert.equal(item.ref, 'tool:call_123');
|
|
140
|
+
assert.equal(item.hash, 'sha256:abc');
|
|
141
|
+
assert.equal(item.chars, 4_000);
|
|
142
|
+
assert.equal(item.compacted, true);
|
|
143
|
+
assert.equal(item.source_chars, 10_000);
|
|
144
|
+
});
|
|
@@ -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,59 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import { classifyOpenAIError, createOpenAIHttpError, diagnosticFromError } from '../src/errors/index.js';
|
|
4
|
+
import { createOpenAIResponsesAdapter } from '../src/providers/openai-responses.js';
|
|
5
|
+
|
|
6
|
+
function response(status, body, headers = {}) { return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json', ...headers } }); }
|
|
7
|
+
|
|
8
|
+
test('OpenAI diagnostics preserve machine code, request id, retry metadata and redact secrets', () => {
|
|
9
|
+
const error = createOpenAIHttpError({
|
|
10
|
+
status: 429,
|
|
11
|
+
body: { error: { type: 'rate_limit_error', code: 'slow_down', message: 'slow down Bearer abc.def.ghi', param: null }, api_key: 'sk-secret-value-123456789' },
|
|
12
|
+
headers: new Headers({ 'retry-after': '2', 'x-request-id': 'req_123' }),
|
|
13
|
+
requestedServiceTier: 'fast',
|
|
14
|
+
});
|
|
15
|
+
assert.equal(error.diagnostic.http_status, 429);
|
|
16
|
+
assert.equal(error.diagnostic.type, 'rate_limit_error');
|
|
17
|
+
assert.equal(error.diagnostic.code, 'slow_down');
|
|
18
|
+
assert.equal(error.diagnostic.request_id, 'req_123');
|
|
19
|
+
assert.equal(error.diagnostic.retry_after_ms, 2000);
|
|
20
|
+
assert.equal(error.diagnostic.retriable, true);
|
|
21
|
+
assert.equal(error.diagnostic.retry_strategy, 'retry_after_backoff');
|
|
22
|
+
assert.equal(error.diagnostic.requested_service_tier, 'fast');
|
|
23
|
+
assert.ok(!JSON.stringify(error.diagnostic).includes('sk-secret-value'));
|
|
24
|
+
assert.ok(!error.diagnostic.message.includes('abc.def.ghi'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test('billing and spend errors are explicitly non-retriable while provider overload is retriable', () => {
|
|
28
|
+
for (const code of ['credit_balance_exhausted', 'organization_spend_limit_exceeded', 'project_spend_limit_exceeded', 'organization_usage_limit_exceeded']) {
|
|
29
|
+
assert.equal(classifyOpenAIError({ status: 429, code }).retriable, false);
|
|
30
|
+
}
|
|
31
|
+
assert.deepEqual(classifyOpenAIError({ status: 503, code: 'server_is_overloaded' }), { category: 'provider_overload', retriable: true, retry_strategy: 'retry_after_backoff' });
|
|
32
|
+
assert.equal(classifyOpenAIError({ status: 401 }).category, 'authentication');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('OpenAI retry classifier distinguishes configuration, continuation, rate, server, and operator-action failures', () => {
|
|
36
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, message: 'Invalid service_tier argument' }), { category: 'service_tier', retriable: false, retry_strategy: 'change_configuration' });
|
|
37
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, code: 'previous_response_not_found' }), { category: 'continuation', retriable: true, retry_strategy: 'retry_full_context' });
|
|
38
|
+
assert.deepEqual(classifyOpenAIError({ status: 400, code: 'websocket_connection_limit_reached' }), { category: 'connection_lifetime', retriable: true, retry_strategy: 'reconnect' });
|
|
39
|
+
assert.deepEqual(classifyOpenAIError({ status: 429, type: 'rate_limit_error', code: 'slow_down' }), { category: 'ramp_rate', retriable: true, retry_strategy: 'retry_after_backoff' });
|
|
40
|
+
assert.deepEqual(classifyOpenAIError({ status: 500 }), { category: 'provider_server', retriable: true, retry_strategy: 'retry_backoff' });
|
|
41
|
+
assert.deepEqual(classifyOpenAIError({ status: 403 }), { category: 'authorization_or_region', retriable: false, retry_strategy: 'operator_action' });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test('Responses adapter throws a diagnostic error instead of flattening the provider response', async () => {
|
|
45
|
+
const adapter = createOpenAIResponsesAdapter({
|
|
46
|
+
apiKey: 'sk-test',
|
|
47
|
+
fetchImpl: async () => response(429, { error: { type: 'insufficient_quota', code: 'project_spend_limit_exceeded', message: 'budget reached' } }, { 'x-request-id': 'req_budget' }),
|
|
48
|
+
});
|
|
49
|
+
await assert.rejects(
|
|
50
|
+
adapter.create({ model: 'gpt-6-astra', reasoningEffort: 'low', serviceTier: 'default', input: 'hi' }),
|
|
51
|
+
(error) => {
|
|
52
|
+
const d = diagnosticFromError(error);
|
|
53
|
+
assert.equal(d.code, 'project_spend_limit_exceeded');
|
|
54
|
+
assert.equal(d.request_id, 'req_budget');
|
|
55
|
+
assert.equal(d.retriable, false);
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
@@ -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,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,42 @@
|
|
|
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
|
+
}, { home });
|
|
31
|
+
|
|
32
|
+
const loaded = loadLocalSession(session.id, { home });
|
|
33
|
+
assert.equal(loaded.provider_state.previous_response_id, 'resp_123');
|
|
34
|
+
assert.equal(loaded.cumulative_usage.cached_input_tokens, 60_544);
|
|
35
|
+
assert.equal(listLocalSessions({ home, cwd })[0].id, session.id);
|
|
36
|
+
|
|
37
|
+
const receipt = renderSessionReceipt(loaded);
|
|
38
|
+
assert.match(receipt, /Token usage: total=21,463 input=21,244 \(\+ 60,544 cached\) output=219 reasoning=31/);
|
|
39
|
+
assert.match(receipt, /Cost: \$0\.1235/);
|
|
40
|
+
assert.match(receipt, new RegExp(`agentsam resume ${session.id}`));
|
|
41
|
+
assert.match(receipt, /Run wrangler whoami/);
|
|
42
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import { describe, it } from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import {
|
|
8
|
+
DEPLOY_INPUT_GLOBS,
|
|
9
|
+
loadOptionalCloudflareEnv,
|
|
10
|
+
resolveLocalStudioDeployable,
|
|
11
|
+
wranglerDeployCommand,
|
|
12
|
+
isLocalStudioCheckout,
|
|
13
|
+
runLocalStudioDeploy,
|
|
14
|
+
} from '../src/lib/deploy/local-studio.js';
|
|
15
|
+
import { productionDeployBlockedReason } from '../src/lib/deploy/git-guard.js';
|
|
16
|
+
|
|
17
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
18
|
+
|
|
19
|
+
describe('local-studio deploy resolver', () => {
|
|
20
|
+
it('resolves apps/local-studio + backend/wrangler.jsonc from the repo', () => {
|
|
21
|
+
const target = resolveLocalStudioDeployable(root);
|
|
22
|
+
assert.equal(target.provider, 'cloudflare');
|
|
23
|
+
assert.equal(target.app, 'local-studio');
|
|
24
|
+
assert.equal(path.basename(path.dirname(target.wranglerConfig)), 'backend');
|
|
25
|
+
assert.deepEqual(target.wranglerArgs, ['deploy', '-c', 'backend/wrangler.jsonc']);
|
|
26
|
+
assert.equal(isLocalStudioCheckout(root), true);
|
|
27
|
+
const cmd = wranglerDeployCommand(target, { dryRun: true });
|
|
28
|
+
assert.equal(cmd.bin, 'npx');
|
|
29
|
+
assert.equal(cmd.cwd, target.appRoot);
|
|
30
|
+
assert.deepEqual(cmd.args, ['wrangler', 'deploy', '-c', 'backend/wrangler.jsonc', '--dry-run']);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('fingerprints the cloudflare connector package', () => {
|
|
34
|
+
assert.equal(DEPLOY_INPUT_GLOBS.includes('packages/connectors/cloudflare'), true);
|
|
35
|
+
assert.equal(DEPLOY_INPUT_GLOBS.includes('apps/local-studio'), true);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('treats missing .env.cloudflare as optional', () => {
|
|
39
|
+
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'cf-env-'));
|
|
40
|
+
const loaded = loadOptionalCloudflareEnv(tmp);
|
|
41
|
+
assert.equal(loaded.loaded, false);
|
|
42
|
+
assert.deepEqual(loaded.vars, {});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('plan does not invoke wrangler', async () => {
|
|
46
|
+
const result = await runLocalStudioDeploy({ cwd: root, planOnly: true, execute: false });
|
|
47
|
+
assert.equal(result.plan.provider, 'cloudflare');
|
|
48
|
+
assert.equal(result.plan.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
|
|
49
|
+
assert.equal(result.plan.genericRootDeploy, false);
|
|
50
|
+
assert.equal(result.receipt.plan, true);
|
|
51
|
+
assert.equal(result.receipt.promoted, false);
|
|
52
|
+
assert.equal(result.receipt.wranglerConfig, 'apps/local-studio/backend/wrangler.jsonc');
|
|
53
|
+
assert.equal(result.receipt.hostname, 'agentsam.inneranimalmedia.com');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('blocks production deploys from feat branches', () => {
|
|
57
|
+
const reason = productionDeployBlockedReason({
|
|
58
|
+
ok: true,
|
|
59
|
+
branch: 'feat/cloudflare-connector-deploy-control',
|
|
60
|
+
detached: false,
|
|
61
|
+
head: 'aaa',
|
|
62
|
+
originMain: 'aaa',
|
|
63
|
+
dirty: false,
|
|
64
|
+
tmpCheckout: false,
|
|
65
|
+
repoRoot: root,
|
|
66
|
+
});
|
|
67
|
+
assert.match(reason, /feat\/cloudflare-connector-deploy-control/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('allows production only when HEAD is a clean origin/main', () => {
|
|
71
|
+
const reason = productionDeployBlockedReason({
|
|
72
|
+
ok: true,
|
|
73
|
+
branch: 'main',
|
|
74
|
+
detached: false,
|
|
75
|
+
head: 'abc',
|
|
76
|
+
originMain: 'abc',
|
|
77
|
+
dirty: false,
|
|
78
|
+
tmpCheckout: false,
|
|
79
|
+
repoRoot: root,
|
|
80
|
+
});
|
|
81
|
+
assert.equal(reason, null);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,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
|
+
});
|