amalgm 0.1.149 → 0.1.150

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.
@@ -0,0 +1,95 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+
6
+ // Stub the event log so requiring snapshot.js never opens a real database.
7
+ const eventsPath = require.resolve('../state/events');
8
+ require.cache[eventsPath] = {
9
+ id: eventsPath,
10
+ filename: eventsPath,
11
+ loaded: true,
12
+ exports: {
13
+ currentSeq: () => 0,
14
+ },
15
+ };
16
+
17
+ const registry = require('../state/registry');
18
+ const snapshot = require('../state/snapshot');
19
+
20
+ test.after(() => {
21
+ delete require.cache[eventsPath];
22
+ });
23
+
24
+ // The registry must reproduce the pre-registry default set exactly — this is
25
+ // the wire contract clients built against.
26
+ const LEGACY_DEFAULT_RESOURCES = ['automations', 'triggers', 'workflows', 'automation_runs', 'workflow_cell_runs', 'agents', 'agent_configs', 'skills', 'apps', 'ports', 'toolbox', 'tools', 'tool_actions', 'hooks', 'projects', 'workspaces', 'project_context', 'user_preferences', 'chat_payloads', 'mcp_connections', 'browser_profiles', 'browser_auth_bundles', 'browser_login_sessions'];
27
+
28
+ test('default snapshot set matches the legacy hardcoded list exactly', () => {
29
+ assert.deepEqual(registry.listDefaultResources(), LEGACY_DEFAULT_RESOURCES);
30
+ assert.deepEqual(snapshot.DEFAULT_RESOURCES, LEGACY_DEFAULT_RESOURCES);
31
+ assert.deepEqual(snapshot.normalizeResources(undefined), LEGACY_DEFAULT_RESOURCES);
32
+ });
33
+
34
+ test('non-default built-ins are registered but excluded from the default set', () => {
35
+ for (const name of ['tasks', 'task_runs', 'event_runs', 'event_triggers', 'uploads', 'browser_surfaces']) {
36
+ assert.equal(registry.hasResource(name), true, `${name} should be registered`);
37
+ assert.equal(registry.listDefaultResources().includes(name), false, `${name} should not be a default`);
38
+ }
39
+ });
40
+
41
+ test('files: prefix resolves for any path under it', () => {
42
+ assert.equal(registry.hasResource('files:workspace/notes'), true);
43
+ assert.equal(registry.hasResource('filesystem'), false);
44
+ });
45
+
46
+ test('unknown resources read as undefined and are omitted from snapshots', () => {
47
+ assert.equal(registry.hasResource('no_such_resource'), false);
48
+ assert.equal(registry.readRegisteredResource('no_such_resource', {}), undefined);
49
+ const result = snapshot.buildSnapshot('no_such_resource');
50
+ assert.deepEqual(result.resources, {});
51
+ assert.equal(result.stable, true);
52
+ });
53
+
54
+ test('a newly registered resource flows through buildSnapshot end to end', () => {
55
+ const rows = [{ id: 'n1', text: 'buy milk' }];
56
+ registry.registerResource('app:demo:notes', { read: () => rows });
57
+ try {
58
+ const result = snapshot.buildSnapshot('app:demo:notes');
59
+ assert.deepEqual(result.resources['app:demo:notes'], rows);
60
+ // Registering does not add it to the default set.
61
+ assert.equal(registry.listDefaultResources().includes('app:demo:notes'), false);
62
+ } finally {
63
+ registry.unregisterResource('app:demo:notes');
64
+ }
65
+ });
66
+
67
+ test('read context is shared within one snapshot pass', () => {
68
+ let reads = 0;
69
+ registry.registerResource('app:demo:shared_a', {
70
+ read: (ctx) => {
71
+ ctx.demoShared ||= (reads += 1, { value: reads });
72
+ return [ctx.demoShared];
73
+ },
74
+ });
75
+ registry.registerResource('app:demo:shared_b', {
76
+ read: (ctx) => {
77
+ ctx.demoShared ||= (reads += 1, { value: reads });
78
+ return [ctx.demoShared];
79
+ },
80
+ });
81
+ try {
82
+ const result = snapshot.buildSnapshot('app:demo:shared_a,app:demo:shared_b');
83
+ assert.equal(reads, 1, 'shared read should run once per attempt');
84
+ assert.deepEqual(result.resources['app:demo:shared_a'], result.resources['app:demo:shared_b']);
85
+ } finally {
86
+ registry.unregisterResource('app:demo:shared_a');
87
+ registry.unregisterResource('app:demo:shared_b');
88
+ }
89
+ });
90
+
91
+ test('duplicate and invalid registrations are rejected', () => {
92
+ assert.throws(() => registry.registerResource('skills', { read: () => [] }), /already registered/);
93
+ assert.throws(() => registry.registerResource('app:demo:bad', {}), /read\(\) function/);
94
+ assert.throws(() => registry.registerResource('', { read: () => [] }), /non-empty string/);
95
+ });
@@ -172,6 +172,11 @@ function normalizeWorkflow(definition) {
172
172
  ? definition.trigger
173
173
  : splitEventRef(definition.trigger || '*.*');
174
174
  const names = new Set();
175
+ if (definition.cells !== undefined && !Array.isArray(definition.cells)) {
176
+ throw new Error(
177
+ `\`cells\` must be an array of cell-builder calls, e.g. cells: [code("step1", fn), tool("step2", "notifications.notify_user", fn)] — got ${typeof definition.cells}.`,
178
+ );
179
+ }
175
180
  const cells = Array.isArray(definition.cells)
176
181
  ? definition.cells.map((cell, index) => normalizeCell(cell, index, names))
177
182
  : [];