@animalabs/agent-framework 0.11.0 → 0.13.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/dist/src/agent.d.ts +11 -1
- package/dist/src/agent.d.ts.map +1 -1
- package/dist/src/agent.js +60 -4
- package/dist/src/agent.js.map +1 -1
- package/dist/src/framework.d.ts +72 -1
- package/dist/src/framework.d.ts.map +1 -1
- package/dist/src/framework.js +873 -123
- package/dist/src/framework.js.map +1 -1
- package/dist/src/gate/event-gate.d.ts +7 -1
- package/dist/src/gate/event-gate.d.ts.map +1 -1
- package/dist/src/gate/event-gate.js +15 -1
- package/dist/src/gate/event-gate.js.map +1 -1
- package/dist/src/kv-unified-wire.d.ts +16 -0
- package/dist/src/kv-unified-wire.d.ts.map +1 -0
- package/dist/src/kv-unified-wire.js +2 -0
- package/dist/src/kv-unified-wire.js.map +1 -0
- package/dist/src/mcpl/channel-registry.d.ts +84 -1
- package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
- package/dist/src/mcpl/channel-registry.js +188 -5
- package/dist/src/mcpl/channel-registry.js.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
- package/dist/src/mcpl/hook-orchestrator.js +28 -2
- package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
- package/dist/src/mcpl/push-handler.d.ts +6 -1
- package/dist/src/mcpl/push-handler.d.ts.map +1 -1
- package/dist/src/mcpl/push-handler.js +29 -2
- package/dist/src/mcpl/push-handler.js.map +1 -1
- package/dist/src/mcpl/reference-fetcher.d.ts +59 -0
- package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -0
- package/dist/src/mcpl/reference-fetcher.js +240 -0
- package/dist/src/mcpl/reference-fetcher.js.map +1 -0
- package/dist/src/mcpl/references.d.ts +125 -0
- package/dist/src/mcpl/references.d.ts.map +1 -0
- package/dist/src/mcpl/references.js +262 -0
- package/dist/src/mcpl/references.js.map +1 -0
- package/dist/src/mcpl/types.d.ts +32 -0
- package/dist/src/mcpl/types.d.ts.map +1 -1
- package/dist/src/mcpl/types.js.map +1 -1
- package/dist/src/modules/index.d.ts +2 -1
- package/dist/src/modules/index.d.ts.map +1 -1
- package/dist/src/modules/index.js +1 -1
- package/dist/src/modules/index.js.map +1 -1
- package/dist/src/modules/workspace/index.d.ts +125 -2
- package/dist/src/modules/workspace/index.d.ts.map +1 -1
- package/dist/src/modules/workspace/index.js +354 -58
- package/dist/src/modules/workspace/index.js.map +1 -1
- package/dist/src/modules/workspace/types.d.ts +7 -0
- package/dist/src/modules/workspace/types.d.ts.map +1 -1
- package/dist/src/modules/workspace/types.js.map +1 -1
- package/dist/src/tool-result-history.d.ts.map +1 -1
- package/dist/src/tool-result-history.js +14 -0
- package/dist/src/tool-result-history.js.map +1 -1
- package/dist/src/tool-wrapper-prose-guard.d.ts +8 -0
- package/dist/src/tool-wrapper-prose-guard.d.ts.map +1 -0
- package/dist/src/tool-wrapper-prose-guard.js +37 -0
- package/dist/src/tool-wrapper-prose-guard.js.map +1 -0
- package/dist/src/tune-out/coordinator.d.ts +144 -0
- package/dist/src/tune-out/coordinator.d.ts.map +1 -0
- package/dist/src/tune-out/coordinator.js +444 -0
- package/dist/src/tune-out/coordinator.js.map +1 -0
- package/dist/src/tune-out/tools.d.ts +43 -0
- package/dist/src/tune-out/tools.d.ts.map +1 -0
- package/dist/src/tune-out/tools.js +93 -0
- package/dist/src/tune-out/tools.js.map +1 -0
- package/dist/src/types/agent.d.ts +15 -0
- package/dist/src/types/agent.d.ts.map +1 -1
- package/dist/src/types/framework.d.ts +6 -0
- package/dist/src/types/framework.d.ts.map +1 -1
- package/dist/src/usage-accounting.d.ts +5 -0
- package/dist/src/usage-accounting.d.ts.map +1 -0
- package/dist/src/usage-accounting.js +11 -0
- package/dist/src/usage-accounting.js.map +1 -0
- package/dist/test/channel-incoming-targeting.test.d.ts +2 -0
- package/dist/test/channel-incoming-targeting.test.d.ts.map +1 -0
- package/dist/test/channel-incoming-targeting.test.js +81 -0
- package/dist/test/channel-incoming-targeting.test.js.map +1 -0
- package/dist/test/conversation-routing.test.js +39 -0
- package/dist/test/conversation-routing.test.js.map +1 -1
- package/dist/test/kv-unified-wiring.test.d.ts +2 -0
- package/dist/test/kv-unified-wiring.test.d.ts.map +1 -0
- package/dist/test/kv-unified-wiring.test.js +56 -0
- package/dist/test/kv-unified-wiring.test.js.map +1 -0
- package/dist/test/mcpl-push-convert.test.d.ts +2 -0
- package/dist/test/mcpl-push-convert.test.d.ts.map +1 -0
- package/dist/test/mcpl-push-convert.test.js +42 -0
- package/dist/test/mcpl-push-convert.test.js.map +1 -0
- package/dist/test/mcpl-reference-fetcher.test.d.ts +2 -0
- package/dist/test/mcpl-reference-fetcher.test.d.ts.map +1 -0
- package/dist/test/mcpl-reference-fetcher.test.js +199 -0
- package/dist/test/mcpl-reference-fetcher.test.js.map +1 -0
- package/dist/test/per-agent-delivery.test.d.ts +2 -0
- package/dist/test/per-agent-delivery.test.d.ts.map +1 -0
- package/dist/test/per-agent-delivery.test.js +108 -0
- package/dist/test/per-agent-delivery.test.js.map +1 -0
- package/dist/test/present-while-acting.test.js +64 -0
- package/dist/test/present-while-acting.test.js.map +1 -1
- package/dist/test/tool-result-history.test.js +78 -0
- package/dist/test/tool-result-history.test.js.map +1 -1
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts +2 -0
- package/dist/test/tool-wrapper-prose-guard-integration.test.d.ts.map +1 -0
- package/dist/test/tool-wrapper-prose-guard-integration.test.js +485 -0
- package/dist/test/tool-wrapper-prose-guard-integration.test.js.map +1 -0
- package/dist/test/tool-wrapper-prose-guard.test.d.ts +2 -0
- package/dist/test/tool-wrapper-prose-guard.test.d.ts.map +1 -0
- package/dist/test/tool-wrapper-prose-guard.test.js +34 -0
- package/dist/test/tool-wrapper-prose-guard.test.js.map +1 -0
- package/dist/test/tune-out-e2e.test.d.ts +2 -0
- package/dist/test/tune-out-e2e.test.d.ts.map +1 -0
- package/dist/test/tune-out-e2e.test.js +142 -0
- package/dist/test/tune-out-e2e.test.js.map +1 -0
- package/dist/test/tune-out-gate-composition.test.d.ts +2 -0
- package/dist/test/tune-out-gate-composition.test.d.ts.map +1 -0
- package/dist/test/tune-out-gate-composition.test.js +221 -0
- package/dist/test/tune-out-gate-composition.test.js.map +1 -0
- package/dist/test/tune-out-lifecycle.test.d.ts +2 -0
- package/dist/test/tune-out-lifecycle.test.d.ts.map +1 -0
- package/dist/test/tune-out-lifecycle.test.js +121 -0
- package/dist/test/tune-out-lifecycle.test.js.map +1 -0
- package/dist/test/turn-trigger-provenance.test.d.ts +2 -0
- package/dist/test/turn-trigger-provenance.test.d.ts.map +1 -0
- package/dist/test/turn-trigger-provenance.test.js +85 -0
- package/dist/test/turn-trigger-provenance.test.js.map +1 -0
- package/dist/test/usage-accounting.test.d.ts +2 -0
- package/dist/test/usage-accounting.test.d.ts.map +1 -0
- package/dist/test/usage-accounting.test.js +13 -0
- package/dist/test/usage-accounting.test.js.map +1 -0
- package/dist/test/workspace-branch-continuation.test.d.ts +15 -0
- package/dist/test/workspace-branch-continuation.test.d.ts.map +1 -0
- package/dist/test/workspace-branch-continuation.test.js +183 -0
- package/dist/test/workspace-branch-continuation.test.js.map +1 -0
- package/dist/test/workspace-read-file-from-disk.test.d.ts +17 -0
- package/dist/test/workspace-read-file-from-disk.test.d.ts.map +1 -0
- package/dist/test/workspace-read-file-from-disk.test.js +174 -0
- package/dist/test/workspace-read-file-from-disk.test.js.map +1 -0
- package/dist/test/workspace-read-image.test.js +8 -0
- package/dist/test/workspace-read-image.test.js.map +1 -1
- package/dist/test/workspace-restore-order.test.js +1 -1
- package/dist/test/workspace-restore-order.test.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/agent.ts +77 -4
- package/src/framework.ts +953 -128
- package/src/gate/event-gate.ts +28 -3
- package/src/kv-unified-wire.ts +12 -0
- package/src/mcpl/channel-registry.ts +264 -8
- package/src/mcpl/hook-orchestrator.ts +29 -2
- package/src/mcpl/push-handler.ts +30 -2
- package/src/mcpl/reference-fetcher.ts +240 -0
- package/src/mcpl/references.ts +292 -0
- package/src/mcpl/types.ts +29 -0
- package/src/modules/index.ts +2 -1
- package/src/modules/workspace/index.ts +425 -66
- package/src/modules/workspace/types.ts +7 -0
- package/src/tool-result-history.ts +11 -0
- package/src/tool-wrapper-prose-guard.ts +38 -0
- package/src/tune-out/coordinator.ts +548 -0
- package/src/tune-out/tools.ts +129 -0
- package/src/types/agent.ts +16 -0
- package/src/types/framework.ts +7 -0
- package/src/usage-accounting.ts +8 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn provenance: the trigger the framework exposes for the turn in progress
|
|
3
|
+
* (getActiveTurnTrigger / the InferenceRequest handed to startAgentStream)
|
|
4
|
+
* must be internally consistent — channel, addressed and counterparty from
|
|
5
|
+
* ONE request — so a host stamping gateway telemetry never reports one
|
|
6
|
+
* person's channel with another person's id.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, it, beforeEach, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { tmpdir } from 'node:os';
|
|
11
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { AgentFramework } from '../src/index.js';
|
|
14
|
+
import { MockMembrane, createMockResponse } from './helpers/mock-membrane.js';
|
|
15
|
+
function internals(framework) {
|
|
16
|
+
return framework;
|
|
17
|
+
}
|
|
18
|
+
describe('Turn trigger provenance', () => {
|
|
19
|
+
let tempDir;
|
|
20
|
+
let membrane;
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
tempDir = mkdtempSync(join(tmpdir(), 'turn-trigger-test-'));
|
|
23
|
+
membrane = new MockMembrane();
|
|
24
|
+
});
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
27
|
+
});
|
|
28
|
+
async function makeFramework() {
|
|
29
|
+
return AgentFramework.create({
|
|
30
|
+
storePath: join(tempDir, 'test.chronicle'),
|
|
31
|
+
membrane: membrane.asMembrane(),
|
|
32
|
+
agents: [{ name: 'scout', model: 'test-model', systemPrompt: 'You are scout.' }],
|
|
33
|
+
modules: [],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
/** Spy on startAgentStream: capture the merged trigger and the value the
|
|
37
|
+
* getter exposes while the turn is alive. */
|
|
38
|
+
function spy(framework) {
|
|
39
|
+
const i = internals(framework);
|
|
40
|
+
const captured = {};
|
|
41
|
+
const orig = i.startAgentStream.bind(framework);
|
|
42
|
+
i.startAgentStream = async (agent, trigger) => {
|
|
43
|
+
captured.handed = trigger;
|
|
44
|
+
const p = orig(agent, trigger);
|
|
45
|
+
captured.exposed = framework.getActiveTurnTrigger('scout');
|
|
46
|
+
return p;
|
|
47
|
+
};
|
|
48
|
+
return captured;
|
|
49
|
+
}
|
|
50
|
+
it('a batch where a later ADDRESSED request wins keeps its channel and author together', async () => {
|
|
51
|
+
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'hi bob' }]));
|
|
52
|
+
const framework = await makeFramework();
|
|
53
|
+
const i = internals(framework);
|
|
54
|
+
const captured = spy(framework);
|
|
55
|
+
const t = Date.now();
|
|
56
|
+
i.pendingRequests.push({ agentName: 'scout', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: t,
|
|
57
|
+
channelId: 'discord:g:alice-room', counterparty: 'discord:user:alice', addressed: false }, { agentName: 'scout', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: t + 1,
|
|
58
|
+
channelId: 'discord:g:bob-room', counterparty: 'discord:user:bob', addressed: true });
|
|
59
|
+
await i.processInferenceRequests();
|
|
60
|
+
await framework.runUntilIdle();
|
|
61
|
+
assert.equal(captured.handed?.channelId, 'discord:g:bob-room', 'addressed channel wins');
|
|
62
|
+
assert.equal(captured.handed?.addressed, true);
|
|
63
|
+
assert.equal(captured.handed?.counterparty, 'discord:user:bob', 'author must come from the SAME request as the channel');
|
|
64
|
+
assert.equal(captured.exposed?.counterparty, 'discord:user:bob', 'getActiveTurnTrigger exposes the same trigger while the turn runs');
|
|
65
|
+
assert.equal(captured.exposed?.channelId, 'discord:g:bob-room');
|
|
66
|
+
assert.equal(framework.getActiveTurnTrigger('scout'), undefined, 'cleared once the turn ended');
|
|
67
|
+
await framework.stop();
|
|
68
|
+
});
|
|
69
|
+
it('a context-budget restart keeps the channel for routing but names no author', async () => {
|
|
70
|
+
membrane.pushResponse(createMockResponse([{ type: 'text', text: 'continuing' }]));
|
|
71
|
+
const framework = await makeFramework();
|
|
72
|
+
const i = internals(framework);
|
|
73
|
+
const captured = spy(framework);
|
|
74
|
+
const t = Date.now();
|
|
75
|
+
i.pendingRequests.push({ agentName: 'scout', reason: 'mcpl:channel-incoming', source: 'discord', timestamp: t,
|
|
76
|
+
channelId: 'discord:g:alice-room', counterparty: 'discord:user:alice', addressed: true }, { agentName: 'scout', reason: 'context_budget_restart', source: 'framework', timestamp: t + 1 });
|
|
77
|
+
await i.processInferenceRequests();
|
|
78
|
+
await framework.runUntilIdle();
|
|
79
|
+
assert.equal(captured.handed?.reason, 'context_budget_restart');
|
|
80
|
+
assert.equal(captured.handed?.channelId, 'discord:g:alice-room', 'routing channel is kept');
|
|
81
|
+
assert.equal(captured.handed?.counterparty, undefined, 'a restart is its own cause — no borrowed author');
|
|
82
|
+
await framework.stop();
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
//# sourceMappingURL=turn-trigger-provenance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"turn-trigger-provenance.test.js","sourceRoot":"","sources":["../../test/turn-trigger-provenance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-accounting.test.d.ts","sourceRoot":"","sources":["../../test/usage-accounting.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { cumulativeDelta } from '../src/usage-accounting.js';
|
|
4
|
+
test('cumulative usage is reduced to the physical provider-call delta', () => {
|
|
5
|
+
assert.equal(cumulativeDelta(120, 0), 120);
|
|
6
|
+
assert.equal(cumulativeDelta(275, 120), 155);
|
|
7
|
+
});
|
|
8
|
+
test('a reset or malformed cumulative usage sample fails conservatively', () => {
|
|
9
|
+
assert.equal(cumulativeDelta(25, 275), 25, 'counter reset is an already-per-call sample');
|
|
10
|
+
assert.equal(cumulativeDelta(Number.NaN, 25), 0);
|
|
11
|
+
assert.equal(cumulativeDelta(-1, 25), 0);
|
|
12
|
+
});
|
|
13
|
+
//# sourceMappingURL=usage-accounting.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usage-accounting.test.js","sourceRoot":"","sources":["../../test/usage-accounting.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,MAAM,MAAM,oBAAoB,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,IAAI,CAAC,iEAAiE,EAAE,GAAG,EAAE;IAC3E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC3C,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;AAC/C,CAAC,CAAC,CAAC;AAEH,IAAI,CAAC,mEAAmE,EAAE,GAAG,EAAE;IAC7E,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,6CAA6C,CAAC,CAAC;IAC1F,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACjD,MAAM,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;AAC3C,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize branch guard: lineage, not identity.
|
|
3
|
+
*
|
|
4
|
+
* An out-of-band repair/treatment branches the store and leaves it on a child
|
|
5
|
+
* branch forked at the parent's head. That child is a linear continuation of
|
|
6
|
+
* exactly the history that was materialized — writing from it can never
|
|
7
|
+
* clobber divergent disk state, so the guard must pass (and heal the pin at
|
|
8
|
+
* boot). Genuine divergence — a branch forked BEFORE the last materialized
|
|
9
|
+
* sequence — must still refuse, with `force: true` as the deliberate
|
|
10
|
+
* override. One mount's stale pin must never block another mount.
|
|
11
|
+
*
|
|
12
|
+
* Regression tests for the Sill 08-24..26 stuck-materialize incident.
|
|
13
|
+
*/
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=workspace-branch-continuation.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-branch-continuation.test.d.ts","sourceRoot":"","sources":["../../test/workspace-branch-continuation.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG"}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Materialize branch guard: lineage, not identity.
|
|
3
|
+
*
|
|
4
|
+
* An out-of-band repair/treatment branches the store and leaves it on a child
|
|
5
|
+
* branch forked at the parent's head. That child is a linear continuation of
|
|
6
|
+
* exactly the history that was materialized — writing from it can never
|
|
7
|
+
* clobber divergent disk state, so the guard must pass (and heal the pin at
|
|
8
|
+
* boot). Genuine divergence — a branch forked BEFORE the last materialized
|
|
9
|
+
* sequence — must still refuse, with `force: true` as the deliberate
|
|
10
|
+
* override. One mount's stale pin must never block another mount.
|
|
11
|
+
*
|
|
12
|
+
* Regression tests for the Sill 08-24..26 stuck-materialize incident.
|
|
13
|
+
*/
|
|
14
|
+
import test from 'node:test';
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { JsStore } from '@animalabs/chronicle';
|
|
20
|
+
import { WorkspaceModule } from '../src/modules/workspace/index.js';
|
|
21
|
+
function makeCtx(opts) {
|
|
22
|
+
return {
|
|
23
|
+
isRestart: opts.isRestart,
|
|
24
|
+
getState: () => (opts.saved ?? null),
|
|
25
|
+
setState: () => { },
|
|
26
|
+
pushEvent: () => { },
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function setup(t, mountNames = ['work']) {
|
|
30
|
+
const root = mkdtempSync(join(tmpdir(), 'af-ws-branch-'));
|
|
31
|
+
const dirs = {};
|
|
32
|
+
for (const name of mountNames) {
|
|
33
|
+
dirs[name] = join(root, name);
|
|
34
|
+
mkdirSync(dirs[name], { recursive: true });
|
|
35
|
+
}
|
|
36
|
+
const store = JsStore.openOrCreate({ path: join(root, 'ws.chronicle') });
|
|
37
|
+
t.after(() => {
|
|
38
|
+
store.close();
|
|
39
|
+
rmSync(root, { recursive: true, force: true });
|
|
40
|
+
});
|
|
41
|
+
const makeModule = () => new WorkspaceModule({
|
|
42
|
+
mounts: mountNames.map((name) => ({
|
|
43
|
+
name, path: dirs[name], mode: 'read-write', watch: 'never',
|
|
44
|
+
})),
|
|
45
|
+
});
|
|
46
|
+
return { store, dirs, makeModule };
|
|
47
|
+
}
|
|
48
|
+
async function statusOf(module, mount = 'work') {
|
|
49
|
+
const res = await module.handleToolCall({ id: 't', name: 'status', input: {} });
|
|
50
|
+
assert.equal(res.success, true, `status failed: ${res.error}`);
|
|
51
|
+
return res.data[mount];
|
|
52
|
+
}
|
|
53
|
+
/** Write a file + materialize on the current branch; returns saved state. */
|
|
54
|
+
async function materializeBaseline(module) {
|
|
55
|
+
let res = await module.handleToolCall({
|
|
56
|
+
id: 'w1', name: 'write', input: { path: 'work/base.txt', content: 'baseline' },
|
|
57
|
+
});
|
|
58
|
+
assert.equal(res.success, true, `write failed: ${res.error}`);
|
|
59
|
+
res = await module.handleToolCall({ id: 'm1', name: 'materialize', input: {} });
|
|
60
|
+
assert.equal(res.success, true, `materialize failed: ${res.error}`);
|
|
61
|
+
const status = await statusOf(module);
|
|
62
|
+
return {
|
|
63
|
+
mounts: {
|
|
64
|
+
work: {
|
|
65
|
+
lastMaterializedSeq: status.lastMaterializedSeq,
|
|
66
|
+
lastMaterializedBranchId: status.lastMaterializedBranch ?? undefined,
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
test('descendant branch (fork at head) materializes without force and re-pins', async (t) => {
|
|
72
|
+
const { store, dirs, makeModule } = setup(t);
|
|
73
|
+
const module = makeModule();
|
|
74
|
+
module.initStore(store);
|
|
75
|
+
await module.start(makeCtx({ isRestart: false }));
|
|
76
|
+
await materializeBaseline(module);
|
|
77
|
+
// Out-of-band-style switch: child forked at the parent's head.
|
|
78
|
+
store.createBranch('repair/test', store.currentBranch().name);
|
|
79
|
+
store.switchBranch('repair/test');
|
|
80
|
+
const childId = store.currentBranch().id;
|
|
81
|
+
let res = await module.handleToolCall({
|
|
82
|
+
id: 'w2', name: 'write', input: { path: 'work/repair.txt', content: 'on child' },
|
|
83
|
+
});
|
|
84
|
+
assert.equal(res.success, true, `write failed: ${res.error}`);
|
|
85
|
+
const before = await statusOf(module);
|
|
86
|
+
assert.equal(before.canMaterialize, true, 'linear continuation must be materializable');
|
|
87
|
+
res = await module.handleToolCall({ id: 'm2', name: 'materialize', input: {} });
|
|
88
|
+
assert.equal(res.success, true, `materialize must pass on descendant branch: ${res.error}`);
|
|
89
|
+
assert.equal(readFileSync(join(dirs.work, 'repair.txt'), 'utf8'), 'on child');
|
|
90
|
+
const after = await statusOf(module);
|
|
91
|
+
assert.equal(after.lastMaterializedBranch, childId, 're-pinned to current branch');
|
|
92
|
+
});
|
|
93
|
+
test('restart after out-of-band branch switch heals the pin at boot (both orders)', async (t) => {
|
|
94
|
+
const { store, makeModule } = setup(t);
|
|
95
|
+
const first = makeModule();
|
|
96
|
+
first.initStore(store);
|
|
97
|
+
await first.start(makeCtx({ isRestart: false }));
|
|
98
|
+
const saved = await materializeBaseline(first);
|
|
99
|
+
const parentId = saved.mounts.work.lastMaterializedBranchId;
|
|
100
|
+
assert.ok(parentId, 'baseline pinned a branch');
|
|
101
|
+
// Offline repair: branch at head, leave the store on the child.
|
|
102
|
+
store.createBranch('repair/offline', store.currentBranch().name);
|
|
103
|
+
store.switchBranch('repair/offline');
|
|
104
|
+
const childId = store.currentBranch().id;
|
|
105
|
+
assert.notEqual(childId, parentId);
|
|
106
|
+
for (const order of ['host', 'reverse']) {
|
|
107
|
+
const module = makeModule();
|
|
108
|
+
if (order === 'host') {
|
|
109
|
+
await module.start(makeCtx({ isRestart: true, saved }));
|
|
110
|
+
module.initStore(store);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
module.initStore(store);
|
|
114
|
+
await module.start(makeCtx({ isRestart: true, saved }));
|
|
115
|
+
}
|
|
116
|
+
const status = await statusOf(module);
|
|
117
|
+
assert.equal(status.lastMaterializedBranch, childId, `${order}: pin healed to current branch`);
|
|
118
|
+
assert.equal(status.canMaterialize, true, `${order}: materializable after heal`);
|
|
119
|
+
const res = await module.handleToolCall({ id: 'm', name: 'materialize', input: {} });
|
|
120
|
+
assert.equal(res.success, true, `${order}: materialize passes after heal: ${res.error}`);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
test('genuinely divergent branch refuses without force, materializes with force', async (t) => {
|
|
124
|
+
const { store, dirs, makeModule } = setup(t);
|
|
125
|
+
const module = makeModule();
|
|
126
|
+
module.initStore(store);
|
|
127
|
+
await module.start(makeCtx({ isRestart: false }));
|
|
128
|
+
// Two materialized generations on main, so a fork between them diverges.
|
|
129
|
+
await materializeBaseline(module);
|
|
130
|
+
const forkAt = store.currentSequence();
|
|
131
|
+
let res = await module.handleToolCall({
|
|
132
|
+
id: 'w2', name: 'write', input: { path: 'work/newer.txt', content: 'newer on main' },
|
|
133
|
+
});
|
|
134
|
+
assert.equal(res.success, true);
|
|
135
|
+
res = await module.handleToolCall({ id: 'm2', name: 'materialize', input: {} });
|
|
136
|
+
assert.equal(res.success, true);
|
|
137
|
+
// Fork BEFORE the last materialized sequence: disk now holds records the
|
|
138
|
+
// child branch never had.
|
|
139
|
+
store.createBranchAt('divergent', store.currentBranch().name, forkAt);
|
|
140
|
+
store.switchBranch('divergent');
|
|
141
|
+
res = await module.handleToolCall({
|
|
142
|
+
id: 'w3', name: 'write', input: { path: 'work/div.txt', content: 'divergent' },
|
|
143
|
+
});
|
|
144
|
+
assert.equal(res.success, true);
|
|
145
|
+
const status = await statusOf(module);
|
|
146
|
+
assert.equal(status.canMaterialize, false, 'divergent branch must not be materializable');
|
|
147
|
+
res = await module.handleToolCall({ id: 'm3', name: 'materialize', input: {} });
|
|
148
|
+
assert.equal(res.success, false, 'divergent branch must refuse without force');
|
|
149
|
+
assert.match(String(res.error), /diverged/);
|
|
150
|
+
assert.match(String(res.error), /force: true/, 'error names the reachable remedy');
|
|
151
|
+
assert.equal(existsSync(join(dirs.work, 'div.txt')), false, 'nothing written on refusal');
|
|
152
|
+
res = await module.handleToolCall({ id: 'm4', name: 'materialize', input: { force: true } });
|
|
153
|
+
assert.equal(res.success, true, `force must override: ${res.error}`);
|
|
154
|
+
assert.equal(readFileSync(join(dirs.work, 'div.txt'), 'utf8'), 'divergent');
|
|
155
|
+
const after = await statusOf(module);
|
|
156
|
+
assert.equal(after.lastMaterializedBranch, store.currentBranch().id, 'force re-pins');
|
|
157
|
+
assert.equal(after.canMaterialize, true, 'materializable again after force');
|
|
158
|
+
});
|
|
159
|
+
test('one mount\'s stale pin does not block other mounts', async (t) => {
|
|
160
|
+
const { store, dirs, makeModule } = setup(t, ['work', 'notes']);
|
|
161
|
+
// Saved state: `work` pinned to a branch that no longer exists (unprovable
|
|
162
|
+
// ancestry → blocked); `notes` never materialized (unpinned → free).
|
|
163
|
+
const saved = {
|
|
164
|
+
mounts: { work: { lastMaterializedSeq: 1, lastMaterializedBranchId: 'branch-gone' } },
|
|
165
|
+
};
|
|
166
|
+
const module = makeModule();
|
|
167
|
+
module.initStore(store);
|
|
168
|
+
await module.start(makeCtx({ isRestart: true, saved }));
|
|
169
|
+
let res = await module.handleToolCall({
|
|
170
|
+
id: 'w1', name: 'write', input: { path: 'notes/note.txt', content: 'note' },
|
|
171
|
+
});
|
|
172
|
+
assert.equal(res.success, true, `write failed: ${res.error}`);
|
|
173
|
+
// Materialize-all: notes must land on disk; work is reported skipped.
|
|
174
|
+
res = await module.handleToolCall({ id: 'm1', name: 'materialize', input: {} });
|
|
175
|
+
assert.equal(res.success, true, `unblocked mount must materialize: ${res.error}`);
|
|
176
|
+
assert.equal(readFileSync(join(dirs.notes, 'note.txt'), 'utf8'), 'note');
|
|
177
|
+
const skipped = res.data.skipped ?? [];
|
|
178
|
+
assert.deepEqual(skipped.map((s) => s.mount), ['work'], 'blocked mount reported as skipped');
|
|
179
|
+
// Targeting the blocked mount alone still refuses.
|
|
180
|
+
res = await module.handleToolCall({ id: 'm2', name: 'materialize', input: { mount: 'work' } });
|
|
181
|
+
assert.equal(res.success, false, 'blocked mount alone must refuse');
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=workspace-branch-continuation.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-branch-continuation.test.js","sourceRoot":"","sources":["../../test/workspace-branch-continuation.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceModule.readFileFromDisk — filesystem-enforced mount containment
|
|
3
|
+
* for peer modules (issue #129).
|
|
4
|
+
*
|
|
5
|
+
* `resolveAbsolutePath()` is lexical: an in-mount symlink targeting an outside
|
|
6
|
+
* file passes it, and a peer that then `fs.readFile`s the returned path reads
|
|
7
|
+
* the outside content (connectome-host #101 pulled such a file into a
|
|
8
|
+
* system-position injection). `readFileFromDisk()` is the workspace-owned read
|
|
9
|
+
* that enforces the mount's `followSymlinks` policy and canonical containment,
|
|
10
|
+
* so peers stop reimplementing the boundary.
|
|
11
|
+
*
|
|
12
|
+
* Symlink cases create real symlinks; on Windows that needs a privilege the
|
|
13
|
+
* test runner may not have (EPERM), in which case those cases are skipped —
|
|
14
|
+
* the non-symlink cases still run everywhere.
|
|
15
|
+
*/
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=workspace-read-file-from-disk.test.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-read-file-from-disk.test.d.ts","sourceRoot":"","sources":["../../test/workspace-read-file-from-disk.test.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WorkspaceModule.readFileFromDisk — filesystem-enforced mount containment
|
|
3
|
+
* for peer modules (issue #129).
|
|
4
|
+
*
|
|
5
|
+
* `resolveAbsolutePath()` is lexical: an in-mount symlink targeting an outside
|
|
6
|
+
* file passes it, and a peer that then `fs.readFile`s the returned path reads
|
|
7
|
+
* the outside content (connectome-host #101 pulled such a file into a
|
|
8
|
+
* system-position injection). `readFileFromDisk()` is the workspace-owned read
|
|
9
|
+
* that enforces the mount's `followSymlinks` policy and canonical containment,
|
|
10
|
+
* so peers stop reimplementing the boundary.
|
|
11
|
+
*
|
|
12
|
+
* Symlink cases create real symlinks; on Windows that needs a privilege the
|
|
13
|
+
* test runner may not have (EPERM), in which case those cases are skipped —
|
|
14
|
+
* the non-symlink cases still run everywhere.
|
|
15
|
+
*/
|
|
16
|
+
import test from 'node:test';
|
|
17
|
+
import assert from 'node:assert/strict';
|
|
18
|
+
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { tmpdir } from 'node:os';
|
|
20
|
+
import { join } from 'node:path';
|
|
21
|
+
import { JsStore } from '@animalabs/chronicle';
|
|
22
|
+
import { WorkspaceModule, WorkspaceReadError } from '../src/modules/workspace/index.js';
|
|
23
|
+
function setup(t, options) {
|
|
24
|
+
const root = mkdtempSync(join(tmpdir(), 'af-read-from-disk-'));
|
|
25
|
+
const mountDir = join(root, 'mount');
|
|
26
|
+
mkdirSync(mountDir, { recursive: true });
|
|
27
|
+
const store = JsStore.openOrCreate({ path: join(root, 'workspace.chronicle') });
|
|
28
|
+
const workspace = new WorkspaceModule({
|
|
29
|
+
mounts: [{ name: 'work', path: mountDir, mode: 'read-write', watch: 'never', followSymlinks: options?.followSymlinks }],
|
|
30
|
+
});
|
|
31
|
+
workspace.initStore(store);
|
|
32
|
+
t.after(() => {
|
|
33
|
+
store.close();
|
|
34
|
+
rmSync(root, { recursive: true, force: true });
|
|
35
|
+
});
|
|
36
|
+
return { root, mountDir, workspace };
|
|
37
|
+
}
|
|
38
|
+
/** Create a symlink, or skip the test when the platform refuses (Windows without the privilege). */
|
|
39
|
+
function symlinkOrSkip(t, target, linkPath, type) {
|
|
40
|
+
try {
|
|
41
|
+
symlinkSync(target, linkPath, type);
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
const code = err.code;
|
|
46
|
+
if (code === 'EPERM' || code === 'EACCES') {
|
|
47
|
+
t.skip(`symlink creation not permitted on this platform (${code})`);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
throw err;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function expectRefusal(promise, code) {
|
|
54
|
+
try {
|
|
55
|
+
await promise;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
assert.ok(err instanceof WorkspaceReadError, `expected WorkspaceReadError, got ${String(err)}`);
|
|
59
|
+
assert.equal(err.code, code, `expected code ${code}, got ${err.code} (${err.message})`);
|
|
60
|
+
return err;
|
|
61
|
+
}
|
|
62
|
+
assert.fail(`expected a WorkspaceReadError(${code}) but the read succeeded`);
|
|
63
|
+
}
|
|
64
|
+
test('a regular in-mount file can be read, with size and mtime reported', async (t) => {
|
|
65
|
+
const { mountDir, workspace } = setup(t);
|
|
66
|
+
writeFileSync(join(mountDir, 'AGENTS.md'), '# instructions\n');
|
|
67
|
+
const result = await workspace.readFileFromDisk('work/AGENTS.md');
|
|
68
|
+
assert.equal(result.bytes.toString('utf8'), '# instructions\n');
|
|
69
|
+
assert.equal(result.size, Buffer.byteLength('# instructions\n'));
|
|
70
|
+
assert.equal(result.truncated, false);
|
|
71
|
+
assert.equal(result.mount, 'work');
|
|
72
|
+
assert.ok(result.mtimeMs > 0);
|
|
73
|
+
assert.ok(result.realPath.endsWith('AGENTS.md'));
|
|
74
|
+
});
|
|
75
|
+
test('nested paths read and reject lexical traversal / unknown mounts by code', async (t) => {
|
|
76
|
+
const { mountDir, workspace } = setup(t);
|
|
77
|
+
mkdirSync(join(mountDir, 'sub'), { recursive: true });
|
|
78
|
+
writeFileSync(join(mountDir, 'sub', 'deep.txt'), 'deep');
|
|
79
|
+
assert.equal((await workspace.readFileFromDisk('work/sub/deep.txt')).bytes.toString(), 'deep');
|
|
80
|
+
const traversal = await expectRefusal(workspace.readFileFromDisk('work/../outside.txt'), 'traversal');
|
|
81
|
+
assert.match(traversal.message, /traversal/i);
|
|
82
|
+
await expectRefusal(workspace.readFileFromDisk('nope/file.txt'), 'unknown_mount');
|
|
83
|
+
});
|
|
84
|
+
test('missing files and directories are distinguished', async (t) => {
|
|
85
|
+
const { mountDir, workspace } = setup(t);
|
|
86
|
+
mkdirSync(join(mountDir, 'dir'));
|
|
87
|
+
await expectRefusal(workspace.readFileFromDisk('work/missing.txt'), 'not_found');
|
|
88
|
+
await expectRefusal(workspace.readFileFromDisk('work/dir'), 'directory');
|
|
89
|
+
await expectRefusal(workspace.readFileFromDisk('work'), 'directory');
|
|
90
|
+
});
|
|
91
|
+
test('a final-component symlink to an outside file is rejected under the default no-follow policy', async (t) => {
|
|
92
|
+
const { root, mountDir, workspace } = setup(t);
|
|
93
|
+
const outside = join(root, 'outside');
|
|
94
|
+
mkdirSync(outside);
|
|
95
|
+
writeFileSync(join(outside, 'secret.txt'), 'SECRET');
|
|
96
|
+
if (!symlinkOrSkip(t, join(outside, 'secret.txt'), join(mountDir, 'AGENTS.md'), 'file'))
|
|
97
|
+
return;
|
|
98
|
+
// The lexical resolver is happy — that is exactly the gap.
|
|
99
|
+
assert.ok(workspace.resolveAbsolutePath('work/AGENTS.md'));
|
|
100
|
+
const err = await expectRefusal(workspace.readFileFromDisk('work/AGENTS.md'), 'symlink');
|
|
101
|
+
assert.ok(!err.message.includes(root), 'refusal must not leak absolute paths');
|
|
102
|
+
});
|
|
103
|
+
test('an intermediate symlinked directory escaping the mount is rejected under no-follow', async (t) => {
|
|
104
|
+
const { root, mountDir, workspace } = setup(t);
|
|
105
|
+
const outside = join(root, 'outside');
|
|
106
|
+
mkdirSync(outside);
|
|
107
|
+
writeFileSync(join(outside, 'secret.txt'), 'SECRET');
|
|
108
|
+
if (!symlinkOrSkip(t, outside, join(mountDir, 'linkdir'), 'dir'))
|
|
109
|
+
return;
|
|
110
|
+
// Final component is a regular file; only the directory hop is a symlink.
|
|
111
|
+
const err = await expectRefusal(workspace.readFileFromDisk('work/linkdir/secret.txt'), 'escape');
|
|
112
|
+
assert.ok(!err.message.includes(root));
|
|
113
|
+
});
|
|
114
|
+
test('with followSymlinks, an in-mount target is followed but an outside target is still an escape', async (t) => {
|
|
115
|
+
const { root, mountDir, workspace } = setup(t, { followSymlinks: true });
|
|
116
|
+
writeFileSync(join(mountDir, 'real.txt'), 'inside');
|
|
117
|
+
const outside = join(root, 'outside');
|
|
118
|
+
mkdirSync(outside);
|
|
119
|
+
writeFileSync(join(outside, 'secret.txt'), 'SECRET');
|
|
120
|
+
if (!symlinkOrSkip(t, join(mountDir, 'real.txt'), join(mountDir, 'alias.txt'), 'file'))
|
|
121
|
+
return;
|
|
122
|
+
if (!symlinkOrSkip(t, join(outside, 'secret.txt'), join(mountDir, 'leak.txt'), 'file'))
|
|
123
|
+
return;
|
|
124
|
+
const followed = await workspace.readFileFromDisk('work/alias.txt');
|
|
125
|
+
assert.equal(followed.bytes.toString(), 'inside');
|
|
126
|
+
assert.ok(followed.realPath.endsWith('real.txt'), 'realPath reports the canonical target');
|
|
127
|
+
await expectRefusal(workspace.readFileFromDisk('work/leak.txt'), 'escape');
|
|
128
|
+
});
|
|
129
|
+
test('with followSymlinks, an in-mount symlink is followed only because policy allows it', async (t) => {
|
|
130
|
+
const strict = setup(t);
|
|
131
|
+
writeFileSync(join(strict.mountDir, 'real.txt'), 'inside');
|
|
132
|
+
if (!symlinkOrSkip(t, join(strict.mountDir, 'real.txt'), join(strict.mountDir, 'alias.txt'), 'file'))
|
|
133
|
+
return;
|
|
134
|
+
// Same layout, default policy: the in-mount symlink is refused as a symlink,
|
|
135
|
+
// not followed — containment would have passed, policy says no.
|
|
136
|
+
await expectRefusal(strict.workspace.readFileFromDisk('work/alias.txt'), 'symlink');
|
|
137
|
+
});
|
|
138
|
+
test('a sibling-prefix directory is not beneath the mount', async (t) => {
|
|
139
|
+
const { root, mountDir, workspace } = setup(t, { followSymlinks: true });
|
|
140
|
+
const sibling = join(root, 'mount-other');
|
|
141
|
+
mkdirSync(sibling);
|
|
142
|
+
writeFileSync(join(sibling, 'x.txt'), 'sibling');
|
|
143
|
+
if (!symlinkOrSkip(t, join(sibling, 'x.txt'), join(mountDir, 'x.txt'), 'file'))
|
|
144
|
+
return;
|
|
145
|
+
// `/mount-other/x.txt` starts with `/mount` as a string prefix — a naive
|
|
146
|
+
// startsWith(root) check would accept it.
|
|
147
|
+
await expectRefusal(workspace.readFileFromDisk('work/x.txt'), 'escape');
|
|
148
|
+
});
|
|
149
|
+
test('maxBytes performs a bounded prefix read and reports truncation', async (t) => {
|
|
150
|
+
const { mountDir, workspace } = setup(t);
|
|
151
|
+
const content = 'x'.repeat(10_000);
|
|
152
|
+
writeFileSync(join(mountDir, 'big.txt'), content);
|
|
153
|
+
const capped = await workspace.readFileFromDisk('work/big.txt', { maxBytes: 1024 });
|
|
154
|
+
assert.equal(capped.truncated, true);
|
|
155
|
+
assert.equal(capped.bytes.length, 1024);
|
|
156
|
+
assert.equal(capped.size, 10_000, 'size reports the full on-disk length');
|
|
157
|
+
assert.equal(capped.bytes.toString(), 'x'.repeat(1024));
|
|
158
|
+
const whole = await workspace.readFileFromDisk('work/big.txt', { maxBytes: 20_000 });
|
|
159
|
+
assert.equal(whole.truncated, false);
|
|
160
|
+
assert.equal(whole.bytes.length, 10_000);
|
|
161
|
+
await assert.rejects(workspace.readFileFromDisk('work/big.txt', { maxBytes: -1 }), RangeError);
|
|
162
|
+
});
|
|
163
|
+
test('read_image keeps its historical error wording after sharing the containment core', async (t) => {
|
|
164
|
+
// The image reader was the original owner of this algorithm; its messages
|
|
165
|
+
// are pinned by workspace-read-image.test.ts. Spot-check the two codes the
|
|
166
|
+
// shared core maps most delicately (not_found variants and directory).
|
|
167
|
+
const { mountDir, workspace } = setup(t);
|
|
168
|
+
mkdirSync(join(mountDir, 'folder'));
|
|
169
|
+
const dir = await workspace.handleToolCall({ id: 'c1', name: 'read_image', input: { path: 'work/folder' } });
|
|
170
|
+
assert.equal(dir.error, 'Path is a directory: work/folder');
|
|
171
|
+
const missing = await workspace.handleToolCall({ id: 'c2', name: 'read_image', input: { path: 'work/nope.png' } });
|
|
172
|
+
assert.equal(missing.error, 'File not found: work/nope.png');
|
|
173
|
+
});
|
|
174
|
+
//# sourceMappingURL=workspace-read-file-from-disk.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workspace-read-file-from-disk.test.js","sourceRoot":"","sources":["../../test/workspace-read-file-from-disk.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -296,6 +296,14 @@ test('valid tiny PNG, JPEG, GIF, and WebP return native image content with exact
|
|
|
296
296
|
expectImageResult(result, path, image);
|
|
297
297
|
}
|
|
298
298
|
});
|
|
299
|
+
test('JPEG with trailing pad bytes after EOI is accepted (hardware encoders pad to 4-byte boundary)', async (t) => {
|
|
300
|
+
const { mountDir, workspace } = setupWorkspace(t);
|
|
301
|
+
// Raspberry Pi camera stills end `ff d9 00 00 00`; decoders stop at EOI.
|
|
302
|
+
const padded = { mimeType: 'image/jpeg', bytes: Buffer.concat([TINY_IMAGES.jpeg.bytes, Buffer.from([0x00, 0x00, 0x00])]) };
|
|
303
|
+
writeFileSync(join(mountDir, 'padded.jpg'), padded.bytes);
|
|
304
|
+
const result = await callReadImage(workspace, 'work/padded.jpg');
|
|
305
|
+
expectImageResult(result, 'work/padded.jpg', padded);
|
|
306
|
+
});
|
|
299
307
|
test('pre-existing Chronicle binary blob is preferred even when the filesystem file is absent', async (t) => {
|
|
300
308
|
const { mountDir, store, workspace, treeStateId } = setupWorkspace(t);
|
|
301
309
|
const image = TINY_IMAGES.png;
|