mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-14 03:56:55 +03:00
test(core): make Windows-flaky shell and npm tests deterministic
This commit is contained in:
committed by
GitHub
Unverified
parent
5ee7f19875
commit
7f2645a8f4
@@ -233,35 +233,32 @@ describe("Npm.add", () => {
|
||||
expect(entries.fallback.entrypoint).toEndWith("/index.js")
|
||||
})
|
||||
|
||||
test("installs and resolves named and unnamed Git packages with dependencies", async () => {
|
||||
test.each(["unnamed", "named"])("installs and resolves %s Git packages with dependencies", async (kind) => {
|
||||
await using tmp = await tmpdir()
|
||||
const fixture = await createGitFixture(tmp.path)
|
||||
const cache = path.join(tmp.path, "cache")
|
||||
const specs = [
|
||||
`git+file://${fixture.repository}#${fixture.commit}`,
|
||||
`fixture-named-plugin@git+file://${fixture.repository}#fixture-branch`,
|
||||
]
|
||||
const spec =
|
||||
kind === "unnamed"
|
||||
? `git+file://${fixture.repository}#${fixture.commit}`
|
||||
: `fixture-named-plugin@git+file://${fixture.repository}#fixture-branch`
|
||||
|
||||
for (const spec of specs) {
|
||||
const entries = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return {
|
||||
added: yield* npm.add(spec),
|
||||
cached: yield* npm.add(spec),
|
||||
resolved: yield* npm.resolve(spec),
|
||||
}
|
||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(cache)), Effect.runPromise)
|
||||
const entries = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
return {
|
||||
added: yield* npm.add(spec),
|
||||
cached: yield* npm.add(spec),
|
||||
resolved: yield* npm.resolve(spec),
|
||||
}
|
||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(path.join(tmp.path, "cache"))), Effect.runPromise)
|
||||
|
||||
expect(entries.added.entrypoint).toBe(pathToFileURL(path.join(entries.added.directory, "index.js")).href)
|
||||
expect(entries.added.version).toBe(fixture.commit)
|
||||
expect(entries.cached).toEqual(entries.added)
|
||||
expect(entries.resolved).toEqual(entries.added)
|
||||
expect(
|
||||
await fs.stat(path.join(path.dirname(entries.added.directory), "fixture-dependency", "package.json")),
|
||||
).toBeTruthy()
|
||||
expect(entries.added.directory).toContain(path.join("npm", await Npm.cacheKey(spec)))
|
||||
expect(entries.added.directory).toContain("node_modules")
|
||||
}
|
||||
expect(entries.added.entrypoint).toBe(pathToFileURL(path.join(entries.added.directory, "index.js")).href)
|
||||
expect(entries.added.version).toBe(fixture.commit)
|
||||
expect(entries.cached).toEqual(entries.added)
|
||||
expect(entries.resolved).toEqual(entries.added)
|
||||
expect(
|
||||
await fs.stat(path.join(path.dirname(entries.added.directory), "fixture-dependency", "package.json")),
|
||||
).toBeTruthy()
|
||||
expect(entries.added.directory).toContain(path.join("npm", await Npm.cacheKey(spec)))
|
||||
expect(entries.added.directory).toContain("node_modules")
|
||||
})
|
||||
|
||||
test("installs a Git package from an npm ::path: subdirectory", async () => {
|
||||
@@ -354,23 +351,19 @@ describe("Npm.resolve", () => {
|
||||
})
|
||||
|
||||
describe("Npm.check and Npm.update", () => {
|
||||
test("checks registry targets without mutation and explicitly updates mutable targets", async () => {
|
||||
test("checks a mutable registry target without mutation and explicitly updates it", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await using registry = await createRegistryFixture(tmp.path)
|
||||
const cache = path.join(tmp.path, "cache")
|
||||
const mutable = "@fixture/registry-plugin@latest"
|
||||
const pinned = "@fixture/registry-plugin@1.0.0"
|
||||
const root = await registry.configure(cache, mutable)
|
||||
await registry.configure(cache, pinned)
|
||||
|
||||
const result = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
const installed = yield* npm.add(mutable)
|
||||
yield* npm.add(pinned)
|
||||
const current = yield* npm.check(mutable)
|
||||
registry.state.latest = "1.1.0"
|
||||
const outdated = yield* npm.check(mutable)
|
||||
const pinnedOutdated = yield* npm.check(pinned)
|
||||
const before = yield* Effect.promise(() => Bun.file(path.join(installed.directory, "index.js")).text())
|
||||
yield* Effect.promise(() => Promise.all([fs.mkdir(path.join(root, "1")), fs.mkdir(path.join(root, "2"))]))
|
||||
const updated = yield* npm.update(mutable)
|
||||
@@ -378,7 +371,6 @@ describe("Npm.check and Npm.update", () => {
|
||||
return {
|
||||
current,
|
||||
outdated,
|
||||
pinnedOutdated,
|
||||
before,
|
||||
after: yield* Effect.promise(() => Bun.file(path.join(updated.directory, "index.js")).text()),
|
||||
version: updated.version,
|
||||
@@ -391,7 +383,6 @@ describe("Npm.check and Npm.update", () => {
|
||||
|
||||
expect(result.current).toBeFalse()
|
||||
expect(result.outdated).toBeTrue()
|
||||
expect(result.pinnedOutdated).toBeFalse()
|
||||
expect(result.before).toContain('version = "1.0.0"')
|
||||
expect(result.after).toContain('version = "1.1.0"')
|
||||
expect(result.version).toBe("1.1.0")
|
||||
@@ -401,4 +392,21 @@ describe("Npm.check and Npm.update", () => {
|
||||
expect(result.generations).not.toContain("2")
|
||||
expect(result.updated).toBeFalse()
|
||||
})
|
||||
|
||||
test("never reports a pinned registry target as outdated", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
await using registry = await createRegistryFixture(tmp.path)
|
||||
const cache = path.join(tmp.path, "cache")
|
||||
const pinned = "@fixture/registry-plugin@1.0.0"
|
||||
await registry.configure(cache, pinned)
|
||||
|
||||
const outdated = await Effect.gen(function* () {
|
||||
const npm = yield* Npm.Service
|
||||
yield* npm.add(pinned)
|
||||
registry.state.latest = "1.1.0"
|
||||
return yield* npm.check(pinned)
|
||||
}).pipe(Effect.scoped, Effect.provide(npmLayer(cache)), Effect.runPromise)
|
||||
|
||||
expect(outdated).toBeFalse()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
import { expect } from "bun:test"
|
||||
import { Deferred, Effect, Fiber, Layer, Queue, Scope, Sink, Stream } from "effect"
|
||||
import { ChildProcessSpawner } from "effect/unstable/process"
|
||||
import { ExitCode, makeHandle, ProcessId } from "effect/unstable/process/ChildProcessSpawner"
|
||||
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
|
||||
import { Config } from "@opencode-ai/core/config"
|
||||
import { Environment } from "@opencode-ai/core/environment/index"
|
||||
import { Location } from "@opencode-ai/core/location"
|
||||
import { Shell } from "@opencode-ai/core/shell"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
import { hostEnvironmentLayer } from "./fixture/environment"
|
||||
import { tempGlobalLayer } from "./fixture/global"
|
||||
import { tempLocationLayer } from "./fixture/location"
|
||||
import { it } from "./lib/effect"
|
||||
|
||||
it.live("eviction makes progress past an already-removed shell", () =>
|
||||
Effect.gen(function* () {
|
||||
const completions = yield* Queue.unbounded<Effect.Effect<void>>()
|
||||
const environment = Layer.effect(
|
||||
Environment.Service,
|
||||
Effect.gen(function* () {
|
||||
const host = yield* Environment.Service
|
||||
const scope = yield* Scope.Scope
|
||||
return Environment.Service.of({
|
||||
...host,
|
||||
spawner: ChildProcessSpawner.make(() =>
|
||||
Effect.gen(function* () {
|
||||
const exited = yield* Deferred.make<ExitCode>()
|
||||
const observer = yield* Deferred.make<Fiber.Fiber<unknown, unknown>>()
|
||||
yield* Queue.offer(
|
||||
completions,
|
||||
Effect.gen(function* () {
|
||||
yield* Deferred.succeed(exited, ExitCode(0))
|
||||
// Shell.wait resolves before retention runs; join the whole exit handler instead.
|
||||
const fiber = yield* Deferred.await(observer)
|
||||
yield* Fiber.join(fiber).pipe(Effect.orDie)
|
||||
}),
|
||||
)
|
||||
const output = Stream.succeed(Buffer.from("hello"))
|
||||
return makeHandle({
|
||||
pid: ProcessId(1),
|
||||
exitCode: Effect.withFiber((fiber) =>
|
||||
Scope.addFinalizer(scope, Fiber.interrupt(fiber)).pipe(
|
||||
Effect.andThen(Deferred.succeed(observer, fiber)),
|
||||
Effect.andThen(Deferred.await(exited)),
|
||||
),
|
||||
),
|
||||
isRunning: Deferred.isDone(exited).pipe(Effect.map((done) => !done)),
|
||||
kill: () => Deferred.succeed(exited, ExitCode(0)).pipe(Effect.asVoid),
|
||||
stdin: Sink.drain,
|
||||
stdout: output,
|
||||
stderr: Stream.empty,
|
||||
all: output,
|
||||
getInputFd: () => Sink.drain,
|
||||
getOutputFd: () => Stream.empty,
|
||||
unref: Effect.succeed(Effect.void),
|
||||
})
|
||||
}),
|
||||
),
|
||||
})
|
||||
}),
|
||||
).pipe(Layer.provide(hostEnvironmentLayer))
|
||||
|
||||
yield* Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
const removed = yield* shell.create({ shell: "sh", command: "removed", timeout: 0 })
|
||||
const finishRemoved = yield* Queue.take(completions)
|
||||
yield* shell.remove(removed.id)
|
||||
expect((yield* shell.result(removed)).capture).toBeUndefined()
|
||||
yield* finishRemoved
|
||||
|
||||
const complete = Effect.gen(function* () {
|
||||
const info = yield* shell.create({ shell: "sh", command: "hello", timeout: 0 })
|
||||
const finish = yield* Queue.take(completions)
|
||||
yield* finish
|
||||
return info
|
||||
})
|
||||
const oldest = yield* complete
|
||||
// Exceed the 25-entry retention cap with the removed ID at the head of exitOrder.
|
||||
yield* Effect.forEach(Array.from({ length: 25 }), () => complete, { discard: true })
|
||||
expect(yield* shell.get(oldest.id).pipe(Effect.flip)).toBeInstanceOf(Shell.NotFoundError)
|
||||
|
||||
const survivor = yield* complete
|
||||
expect(yield* shell.result(survivor)).toMatchObject({
|
||||
info: { status: "exited", exit: 0 },
|
||||
capture: { output: "hello", truncated: false },
|
||||
})
|
||||
}).pipe(
|
||||
Effect.provide(
|
||||
AppNodeBuilder.build(Shell.node, [
|
||||
Location.node.replace(tempLocationLayer),
|
||||
Global.node.replace(tempGlobalLayer),
|
||||
Config.node.replace(Config.testLayer()),
|
||||
Environment.node.replace(environment),
|
||||
]),
|
||||
),
|
||||
)
|
||||
}),
|
||||
)
|
||||
@@ -1510,35 +1510,6 @@ describe("ShellTool", () => {
|
||||
{ timeout: 30_000 },
|
||||
)
|
||||
|
||||
it.live("does not retain removed running shells in exit order", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) =>
|
||||
withSession(tmp.path, () =>
|
||||
Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
yield* Effect.forEach(Array.from({ length: 26 }), () =>
|
||||
Effect.gen(function* () {
|
||||
const info = yield* shell.create({ command: idleCommand, timeout: 0 })
|
||||
yield* shell.remove(info.id)
|
||||
expect((yield* shell.result(info)).capture).toBeUndefined()
|
||||
yield* Effect.sleep(Duration.millis(10))
|
||||
}),
|
||||
)
|
||||
|
||||
const info = yield* shell.create({ command: helloCommand, timeout: 0 })
|
||||
const settled = yield* shell.wait(info.id).pipe(Effect.timeoutOption(Duration.seconds(2)))
|
||||
expect(settled._tag).toBe("Some")
|
||||
expect(yield* shell.result(info)).toMatchObject({
|
||||
info: { status: "exited", exit: 0 },
|
||||
capture: { output: expect.stringContaining("hello"), truncated: false },
|
||||
})
|
||||
}),
|
||||
),
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
)
|
||||
|
||||
if (!isWindows) {
|
||||
it.live("settles a shell terminated by an external signal", () =>
|
||||
Effect.acquireUseRelease(
|
||||
|
||||
Reference in New Issue
Block a user