|
@@ -1,7 +1,7 @@
|
|
|
/**
|
|
/**
|
|
|
* The `file` provider's frame stream: how the two address scopes resolve to a
|
|
* The `file` provider's frame stream: how the two address scopes resolve to a
|
|
|
- * Host call and a change-feed key, the opening stat, the write flag that
|
|
|
|
|
- * carries no content, the reload and disappearance that stat again, failures
|
|
|
|
|
|
|
+ * Host call and a change-feed key, the opening stat, the write version that
|
|
|
|
|
+ * carries no content, the disappearance that stats again, failures
|
|
|
* as frames, and the life bounded by the signal.
|
|
* as frames, and the life bounded by the signal.
|
|
|
*/
|
|
*/
|
|
|
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
|
import { RemoteError } from '@deepseek-ai/dsh-client-test-runtime'
|
|
@@ -12,8 +12,7 @@ import type { WorkspaceFileStat } from '../src/types.ts'
|
|
|
import { describe, expect, it, onTestFinished } from 'vitest'
|
|
import { describe, expect, it, onTestFinished } from 'vitest'
|
|
|
import { ChangeFeed } from '../src/client/change-feed.ts'
|
|
import { ChangeFeed } from '../src/client/change-feed.ts'
|
|
|
import { createFileResourceProvider } from '../src/client/provider.ts'
|
|
import { createFileResourceProvider } from '../src/client/provider.ts'
|
|
|
-import type { SessionLookup } from '../src/client/provider.ts'
|
|
|
|
|
-import { FakeRemote, peek, settle } from './fake-remote.client.ts'
|
|
|
|
|
|
|
+import { FakeRemote, settle } from './fake-remote.client.ts'
|
|
|
|
|
|
|
|
const S1 = 's1' as SessionId
|
|
const S1 = 's1' as SessionId
|
|
|
const S2 = 's2' as SessionId
|
|
const S2 = 's2' as SessionId
|
|
@@ -23,23 +22,17 @@ const HOST_PATH = '/w/a b.txt'
|
|
|
const ADDRESS = sessionFileAddress(S1, REL_PATH)
|
|
const ADDRESS = sessionFileAddress(S1, REL_PATH)
|
|
|
/** A file outside every workspace root, addressed absolutely. */
|
|
/** A file outside every workspace root, addressed absolutely. */
|
|
|
const ABS_PATH = '/etc/hosts'
|
|
const ABS_PATH = '/etc/hosts'
|
|
|
-const ABS_ADDRESS = absoluteFileAddress(ABS_PATH)
|
|
|
|
|
-
|
|
|
|
|
-/** Only the current Session is available to the provider; there are no Client roots. */
|
|
|
|
|
-function sessionsWith(current: SessionId | undefined): SessionLookup {
|
|
|
|
|
- return { current: () => current }
|
|
|
|
|
-}
|
|
|
|
|
|
|
+const ABS_ADDRESS = sessionFileAddress(S1, ABS_PATH)
|
|
|
|
|
|
|
|
const stat = (version: string, bytes: number): WorkspaceFileStat => ({ absolutePath: HOST_PATH, version, bytes })
|
|
const stat = (version: string, bytes: number): WorkspaceFileStat => ({ absolutePath: HOST_PATH, version, bytes })
|
|
|
const notFound = (): RemoteFailure => new RemoteError('workspace-file/not-found', 'no such file', { path: REL_PATH })
|
|
const notFound = (): RemoteFailure => new RemoteError('workspace-file/not-found', 'no such file', { path: REL_PATH })
|
|
|
|
|
|
|
|
-function opened(address = ADDRESS, sessions = sessionsWith(S1)) {
|
|
|
|
|
|
|
+function opened(address = ADDRESS) {
|
|
|
const remote = new FakeRemote()
|
|
const remote = new FakeRemote()
|
|
|
const changes = new ChangeFeed(remote)
|
|
const changes = new ChangeFeed(remote)
|
|
|
- const provider = createFileResourceProvider(remote, changes, sessions)
|
|
|
|
|
|
|
+ const provider = createFileResourceProvider(remote, changes)
|
|
|
const controller = new AbortController()
|
|
const controller = new AbortController()
|
|
|
const it = provider.open(address, { signal: controller.signal })[Symbol.asyncIterator]()
|
|
const it = provider.open(address, { signal: controller.signal })[Symbol.asyncIterator]()
|
|
|
- const reload = (): void => { provider.reload!(address) }
|
|
|
|
|
onTestFinished(async () => {
|
|
onTestFinished(async () => {
|
|
|
controller.abort()
|
|
controller.abort()
|
|
|
for (const request of remote.stats) {
|
|
for (const request of remote.stats) {
|
|
@@ -48,7 +41,7 @@ function opened(address = ADDRESS, sessions = sessionsWith(S1)) {
|
|
|
await it.return?.()
|
|
await it.return?.()
|
|
|
await changes.settle()
|
|
await changes.settle()
|
|
|
})
|
|
})
|
|
|
- return { remote, provider, changes, controller, it, reload }
|
|
|
|
|
|
|
+ return { remote, provider, changes, controller, it }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** Open, answer the opening stat, and hand back the bench once the first frame is out. */
|
|
/** Open, answer the opening stat, and hand back the bench once the first frame is out. */
|
|
@@ -80,9 +73,9 @@ describe('file provider — the address', () => {
|
|
|
expect(remote.opened).toEqual([])
|
|
expect(remote.opened).toEqual([])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('rejects an absolute address with no current Session without touching the Remote', async () => {
|
|
|
|
|
- const address = ABS_ADDRESS
|
|
|
|
|
- const { remote, it } = opened(address, sessionsWith(undefined))
|
|
|
|
|
|
|
+ it('rejects an absolute address with no Session without touching the Remote', async () => {
|
|
|
|
|
+ const address = absoluteFileAddress(ABS_PATH)
|
|
|
|
|
+ const { remote, it } = opened(address)
|
|
|
const first = await it.next()
|
|
const first = await it.next()
|
|
|
expect(first.done).toBe(false)
|
|
expect(first.done).toBe(false)
|
|
|
expect(first.value).toMatchObject({ ok: false, error: { code: 'workspace-file/unknown-workspace', details: { address } } })
|
|
expect(first.value).toMatchObject({ ok: false, error: { code: 'workspace-file/unknown-workspace', details: { address } } })
|
|
@@ -91,14 +84,6 @@ describe('file provider — the address', () => {
|
|
|
expect(remote.opened).toEqual([])
|
|
expect(remote.opened).toEqual([])
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('reloads nothing for an address it does not resolve', async () => {
|
|
|
|
|
- for (const bench of [opened('dsh-resource://file/shared/x/w/a.txt'), opened(ABS_ADDRESS, sessionsWith(undefined))]) {
|
|
|
|
|
- bench.reload()
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(bench.remote.stats).toEqual([])
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
it('hands the Host a session address\'s relative path and follows the stat absolute path', async () => {
|
|
it('hands the Host a session address\'s relative path and follows the stat absolute path', async () => {
|
|
|
const { remote, it } = opened()
|
|
const { remote, it } = opened()
|
|
|
const first = it.next()
|
|
const first = it.next()
|
|
@@ -108,31 +93,24 @@ describe('file provider — the address', () => {
|
|
|
await first
|
|
await first
|
|
|
// The Host's frame names the file absolutely; the follower keyed by the resolved path receives it.
|
|
// The Host's frame names the file absolutely; the follower keyed by the resolved path receives it.
|
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
- await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
|
|
+ await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1' } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('reads an absolute address through the Session on screen, with the absolute path as both Host path and follow key', async () => {
|
|
|
|
|
- const { remote, it, reload } = opened(ABS_ADDRESS)
|
|
|
|
|
|
|
+ it('reads an absolute path through the Session in its address, with the absolute path as both Host path and follow key', async () => {
|
|
|
|
|
+ const { remote, it } = opened(ABS_ADDRESS)
|
|
|
const first = it.next()
|
|
const first = it.next()
|
|
|
await settle()
|
|
await settle()
|
|
|
expect(remote.opened.map(o => o.sessionId)).toEqual([S1])
|
|
expect(remote.opened.map(o => o.sessionId)).toEqual([S1])
|
|
|
expect(remote.stats[0]).toMatchObject({ sessionId: S1, path: ABS_PATH })
|
|
expect(remote.stats[0]).toMatchObject({ sessionId: S1, path: ABS_PATH })
|
|
|
remote.stats[0]!.resolve({ ok: true, value: { absolutePath: ABS_PATH, version: 'v0', bytes: 3 } })
|
|
remote.stats[0]!.resolve({ ok: true, value: { absolutePath: ABS_PATH, version: 'v0', bytes: 3 } })
|
|
|
- await expect(first).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: ABS_PATH, version: 'v0', bytes: 3, changed: false } } })
|
|
|
|
|
|
|
+ await expect(first).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: ABS_PATH, version: 'v0', bytes: 3 } } })
|
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: ABS_PATH, version: 'v1' } })
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: ABS_PATH, version: 'v1' } })
|
|
|
- await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
- // A reload stats the same absolute path again.
|
|
|
|
|
- reload()
|
|
|
|
|
- const next = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(remote.stats[1]).toMatchObject({ sessionId: S1, path: ABS_PATH })
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: true, value: { absolutePath: ABS_PATH, version: 'v1', bytes: 4 } })
|
|
|
|
|
- await expect(next).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: ABS_PATH, version: 'v1', bytes: 4, changed: false } } })
|
|
|
|
|
|
|
+ await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1' } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('opens one change stream per session named by the addresses', async () => {
|
|
it('opens one change stream per session named by the addresses', async () => {
|
|
|
const remote = new FakeRemote()
|
|
const remote = new FakeRemote()
|
|
|
- const provider = createFileResourceProvider(remote, new ChangeFeed(remote), sessionsWith(S1))
|
|
|
|
|
|
|
+ const provider = createFileResourceProvider(remote, new ChangeFeed(remote))
|
|
|
const signal = new AbortController().signal
|
|
const signal = new AbortController().signal
|
|
|
void provider.open(sessionFileAddress(S1, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
void provider.open(sessionFileAddress(S1, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
|
void provider.open(sessionFileAddress(S2, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
void provider.open(sessionFileAddress(S2, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
@@ -143,14 +121,14 @@ describe('file provider — the address', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('file provider — the opening stat', () => {
|
|
describe('file provider — the opening stat', () => {
|
|
|
- it('stats the decoded relative path in the session the address names and yields its metadata unflagged', async () => {
|
|
|
|
|
|
|
+ it('stats the decoded relative path in the session the address names and yields its metadata', async () => {
|
|
|
const { remote, it, controller } = opened()
|
|
const { remote, it, controller } = opened()
|
|
|
const first = it.next()
|
|
const first = it.next()
|
|
|
await settle()
|
|
await settle()
|
|
|
expect(remote.stats).toHaveLength(1)
|
|
expect(remote.stats).toHaveLength(1)
|
|
|
expect(remote.stats[0]).toMatchObject({ sessionId: S1, path: REL_PATH, signal: controller.signal })
|
|
expect(remote.stats[0]).toMatchObject({ sessionId: S1, path: REL_PATH, signal: controller.signal })
|
|
|
remote.stats[0]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
remote.stats[0]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
|
- await expect(first).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v0', bytes: 3, changed: false } } })
|
|
|
|
|
|
|
+ await expect(first).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v0', bytes: 3 } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('omits bytes when the backend reports none', async () => {
|
|
it('omits bytes when the backend reports none', async () => {
|
|
@@ -158,7 +136,7 @@ describe('file provider — the opening stat', () => {
|
|
|
const first = it.next()
|
|
const first = it.next()
|
|
|
await settle()
|
|
await settle()
|
|
|
remote.stats[0]!.resolve({ ok: true, value: { absolutePath: HOST_PATH, version: 'v0' } })
|
|
remote.stats[0]!.resolve({ ok: true, value: { absolutePath: HOST_PATH, version: 'v0' } })
|
|
|
- await expect(first).resolves.toStrictEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v0', changed: false } } })
|
|
|
|
|
|
|
+ await expect(first).resolves.toStrictEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v0' } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('yields the Host failure as a frame and keeps following the address', async () => {
|
|
it('yields the Host failure as a frame and keeps following the address', async () => {
|
|
@@ -174,24 +152,11 @@ describe('file provider — the opening stat', () => {
|
|
|
const pending = it.next()
|
|
const pending = it.next()
|
|
|
await expect(Promise.race([pending, settle().then(() => 'silent' as const)])).resolves.toBe('silent')
|
|
await expect(Promise.race([pending, settle().then(() => 'silent' as const)])).resolves.toBe('silent')
|
|
|
expect(remote.stats).toHaveLength(1)
|
|
expect(remote.stats).toHaveLength(1)
|
|
|
- // The agent creates the file: the write stats again and the value goes live, flagged as changed.
|
|
|
|
|
|
|
+ // The agent creates the file: the write stats again and the value goes live.
|
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
await settle()
|
|
await settle()
|
|
|
remote.stats[1]!.resolve({ ok: true, value: stat('v1', 5) })
|
|
remote.stats[1]!.resolve({ ok: true, value: stat('v1', 5) })
|
|
|
- await expect(pending).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 5, changed: true } } })
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('lets a reload retry a failed opening stat', async () => {
|
|
|
|
|
- const { remote, it, reload } = opened()
|
|
|
|
|
- const first = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- remote.stats[0]!.resolve({ ok: false, error: notFound() })
|
|
|
|
|
- await first
|
|
|
|
|
- reload()
|
|
|
|
|
- const next = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
|
|
|
- await expect(next).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v0', bytes: 3, changed: false } } })
|
|
|
|
|
|
|
+ await expect(pending).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 5 } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('ends without a frame when aborted during the stat', async () => {
|
|
it('ends without a frame when aborted during the stat', async () => {
|
|
@@ -205,7 +170,7 @@ describe('file provider — the opening stat', () => {
|
|
|
|
|
|
|
|
it('shares one change stream between two files of a session', async () => {
|
|
it('shares one change stream between two files of a session', async () => {
|
|
|
const remote = new FakeRemote()
|
|
const remote = new FakeRemote()
|
|
|
- const provider = createFileResourceProvider(remote, new ChangeFeed(remote), sessionsWith(S1))
|
|
|
|
|
|
|
+ const provider = createFileResourceProvider(remote, new ChangeFeed(remote))
|
|
|
const signal = new AbortController().signal
|
|
const signal = new AbortController().signal
|
|
|
void provider.open(sessionFileAddress(S1, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
void provider.open(sessionFileAddress(S1, 'a.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
|
void provider.open(sessionFileAddress(S1, 'b.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
void provider.open(sessionFileAddress(S1, 'b.txt'), { signal })[Symbol.asyncIterator]().next()
|
|
@@ -216,10 +181,10 @@ describe('file provider — the opening stat', () => {
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('file provider — Host writes', () => {
|
|
describe('file provider — Host writes', () => {
|
|
|
- it('flags a write with its version and keeps the byte count', async () => {
|
|
|
|
|
|
|
+ it('reports a write with its version and keeps the byte count', async () => {
|
|
|
const { remote, it } = await live()
|
|
const { remote, it } = await live()
|
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
- await expect(it.next()).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 3, changed: true } } })
|
|
|
|
|
|
|
+ await expect(it.next()).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 3 } } })
|
|
|
expect(remote.stats).toHaveLength(1)
|
|
expect(remote.stats).toHaveLength(1)
|
|
|
})
|
|
})
|
|
|
|
|
|
|
@@ -231,7 +196,7 @@ describe('file provider — Host writes', () => {
|
|
|
const pending = it.next()
|
|
const pending = it.next()
|
|
|
await expect(Promise.race([pending, settle().then(() => 'silent' as const)])).resolves.toBe('silent')
|
|
await expect(Promise.race([pending, settle().then(() => 'silent' as const)])).resolves.toBe('silent')
|
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
- await expect(pending).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
|
|
+ await expect(pending).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1' } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
it('does not lose a write reported during the opening stat', async () => {
|
|
it('does not lose a write reported during the opening stat', async () => {
|
|
@@ -241,23 +206,35 @@ describe('file provider — Host writes', () => {
|
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
await settle()
|
|
await settle()
|
|
|
remote.stats[0]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
remote.stats[0]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
|
- await expect(first).resolves.toMatchObject({ value: { ok: true, value: { version: 'v0', changed: false } } })
|
|
|
|
|
- await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
|
|
+ await expect(first).resolves.toMatchObject({ value: { ok: true, value: { version: 'v0' } } })
|
|
|
|
|
+ await expect(it.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1' } } })
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
describe('file provider — a reported disappearance', () => {
|
|
describe('file provider — a reported disappearance', () => {
|
|
|
- it('stats again and, when the file is still there, yields its fresh metadata flagged', async () => {
|
|
|
|
|
|
|
+ it('ends quietly when aborted during the stat', async () => {
|
|
|
|
|
+ const { remote, it, controller, changes } = await live()
|
|
|
|
|
+ const next = it.next()
|
|
|
|
|
+ await remote.opened[0]!.source.deliver({ kind: 'change', change: { absolutePath: HOST_PATH, absent: true } })
|
|
|
|
|
+ const request = await remote.waitForStat(1)
|
|
|
|
|
+ controller.abort()
|
|
|
|
|
+ request.resolve({ ok: true, value: stat('late', 9) })
|
|
|
|
|
+ await expect(next).resolves.toEqual({ done: true, value: undefined })
|
|
|
|
|
+ await changes.settle()
|
|
|
|
|
+ expect(remote.disposed).toEqual(['workspace file changes of s1'])
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ it('stats again and, when the file is still there, yields its fresh metadata', async () => {
|
|
|
const { remote, it } = await live('v0', 3)
|
|
const { remote, it } = await live('v0', 3)
|
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, absent: true } })
|
|
remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, absent: true } })
|
|
|
const next = it.next()
|
|
const next = it.next()
|
|
|
await settle()
|
|
await settle()
|
|
|
expect(remote.stats).toHaveLength(2)
|
|
expect(remote.stats).toHaveLength(2)
|
|
|
remote.stats[1]!.resolve({ ok: true, value: stat('v2', 9) })
|
|
remote.stats[1]!.resolve({ ok: true, value: stat('v2', 9) })
|
|
|
- await expect(next).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v2', bytes: 9, changed: true } } })
|
|
|
|
|
|
|
+ await expect(next).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v2', bytes: 9 } } })
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
- it('yields the not-found frame and keeps following, so a later write stats again and brings the file back flagged', async () => {
|
|
|
|
|
|
|
+ it('yields the not-found frame and keeps following, so a later write stats again and brings the file back', async () => {
|
|
|
const { remote, it } = await live('v0', 3)
|
|
const { remote, it } = await live('v0', 3)
|
|
|
const source = remote.opened[0]!.source
|
|
const source = remote.opened[0]!.source
|
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, absent: true } })
|
|
source.push({ kind: 'change', change: { absolutePath: HOST_PATH, absent: true } })
|
|
@@ -270,79 +247,7 @@ describe('file provider — a reported disappearance', () => {
|
|
|
const back = it.next()
|
|
const back = it.next()
|
|
|
await settle()
|
|
await settle()
|
|
|
remote.stats[2]!.resolve({ ok: true, value: stat('v3', 8) })
|
|
remote.stats[2]!.resolve({ ok: true, value: stat('v3', 8) })
|
|
|
- await expect(back).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v3', bytes: 8, changed: true } } })
|
|
|
|
|
- })
|
|
|
|
|
-})
|
|
|
|
|
-
|
|
|
|
|
-describe('file provider — reload', () => {
|
|
|
|
|
- it('stats again and clears the flag', async () => {
|
|
|
|
|
- const { remote, it, reload } = await live('v0', 3)
|
|
|
|
|
- remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
|
|
- await it.next()
|
|
|
|
|
- reload()
|
|
|
|
|
- const next = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(remote.stats).toHaveLength(2)
|
|
|
|
|
- expect(remote.stats[1]).toMatchObject({ sessionId: S1, path: REL_PATH })
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: true, value: stat('v1', 7) })
|
|
|
|
|
- await expect(next).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 7, changed: false } } })
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('yields a failed stat as a frame and keeps the stream open', async () => {
|
|
|
|
|
- const { remote, it, reload } = await live()
|
|
|
|
|
- reload()
|
|
|
|
|
- const next = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- const error = notFound()
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: false, error })
|
|
|
|
|
- await expect(next).resolves.toEqual({ done: false, value: { ok: false, error } })
|
|
|
|
|
- await expect(peek(it)).resolves.toBe('silent')
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('ends quietly when aborted during the stat', async () => {
|
|
|
|
|
- const { remote, it, reload, controller } = await live()
|
|
|
|
|
- reload()
|
|
|
|
|
- const next = it.next()
|
|
|
|
|
- await settle()
|
|
|
|
|
- controller.abort()
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: false, error: new RemoteError('gateway/internal', 'aborted', {}) })
|
|
|
|
|
- await expect(next).resolves.toEqual({ done: true, value: undefined })
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('re-stats every record of the path on one record\'s reload: a session record and an absolute record of one file share it', async () => {
|
|
|
|
|
- const remote = new FakeRemote()
|
|
|
|
|
- const provider = createFileResourceProvider(remote, new ChangeFeed(remote), sessionsWith(S1))
|
|
|
|
|
- const signal = new AbortController().signal
|
|
|
|
|
- const session = provider.open(ADDRESS, { signal })[Symbol.asyncIterator]()
|
|
|
|
|
- const absolute = provider.open(absoluteFileAddress(HOST_PATH), { signal })[Symbol.asyncIterator]()
|
|
|
|
|
- const firsts = Promise.all([session.next(), absolute.next()])
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(remote.stats.map(pending => [pending.sessionId, pending.path])).toEqual([[S1, REL_PATH], [S1, HOST_PATH]])
|
|
|
|
|
- remote.stats[0]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
|
|
|
- remote.stats[1]!.resolve({ ok: true, value: stat('v0', 3) })
|
|
|
|
|
- await firsts
|
|
|
|
|
- expect(remote.opened).toHaveLength(1)
|
|
|
|
|
- // One Host write flags both records: they follow one path.
|
|
|
|
|
- remote.opened[0]!.source.push({ kind: 'change', change: { absolutePath: HOST_PATH, version: 'v1' } })
|
|
|
|
|
- await expect(session.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
- await expect(absolute.next()).resolves.toMatchObject({ value: { ok: true, value: { version: 'v1', changed: true } } })
|
|
|
|
|
- // A reload on the session record re-stats both and clears both flags:
|
|
|
|
|
- // delivery is per path, not per record.
|
|
|
|
|
- provider.reload!(ADDRESS)
|
|
|
|
|
- const nexts = Promise.all([session.next(), absolute.next()])
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(remote.stats.slice(2).map(pending => pending.path)).toEqual([REL_PATH, HOST_PATH])
|
|
|
|
|
- remote.stats[2]!.resolve({ ok: true, value: stat('v1', 3) })
|
|
|
|
|
- remote.stats[3]!.resolve({ ok: true, value: stat('v1', 3) })
|
|
|
|
|
- const cleared = { done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v1', bytes: 3, changed: false } } }
|
|
|
|
|
- await expect(nexts).resolves.toEqual([cleared, cleared])
|
|
|
|
|
- })
|
|
|
|
|
-
|
|
|
|
|
- it('is a no-op for a file nobody has open', async () => {
|
|
|
|
|
- const { remote, reload } = opened()
|
|
|
|
|
- reload()
|
|
|
|
|
- await settle()
|
|
|
|
|
- expect(remote.stats).toHaveLength(0)
|
|
|
|
|
|
|
+ await expect(back).resolves.toEqual({ done: false, value: { ok: true, value: { absolutePath: HOST_PATH, version: 'v3', bytes: 8 } } })
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|