Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions integration-tests/cli/qwen-serve-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ describe('qwen serve — capabilities envelope', () => {
'session_events',
'session_artifacts',
'session_artifacts_persistence',
'session_artifacts_content_retention',
'slow_client_warning',
'typed_event_schema',
'session_set_model',
Expand Down
364 changes: 364 additions & 0 deletions packages/acp-bridge/src/bridge.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ import {
WS_B,
SESS_A,
} from './internal/testUtils.js';
import { SessionArtifactContentStore } from './sessionArtifactContentStore.js';
import { SessionArtifactAuthorizationError } from './sessionArtifacts.js';

function deferred<T>(): {
Expand Down Expand Up @@ -195,6 +196,41 @@ describe('createAcpSessionBridge', () => {
}
});

it('validates artifact remove options before changing live state', async () => {
const bridge = makeBridge({
channelFactory: async () => makeChannel().channel,
});
const session = await bridge.spawnOrAttach({ workspaceCwd: WS_A });
try {
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Client link',
url: 'https://example.com/client',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;

await expect(
bridge.removeSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ deleteContent: 'yes' } as unknown as { deleteContent: boolean },
),
).rejects.toThrow(/deleteContent must be a boolean/);

await expect(
bridge.getSessionArtifacts(session.sessionId),
).resolves.toMatchObject({
artifacts: [{ id: artifactId }],
});
} finally {
await bridge.shutdown();
}
});

it('rejects invalid client artifact records instead of dropping them', async () => {
const bridge = makeBridge({
channelFactory: async () => makeChannel().channel,
Expand All @@ -216,6 +252,329 @@ describe('createAcpSessionBridge', () => {
}
});

it('does not prune or GC expired pins during artifact GET', async () => {
const previousQwenHome = process.env['QWEN_HOME'];
const tempHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'qwen-home-'));
const workspace = await fsp.mkdtemp(
path.join(os.tmpdir(), 'qwen-artifact-workspace-'),
);
process.env['QWEN_HOME'] = tempHome;
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-07-04T00:00:00.000Z'));
const gcSpy = vi.spyOn(SessionArtifactContentStore.prototype, 'gc');
const bridge = makeBridge({
boundWorkspace: workspace,
channelFactory: async () => makeChannel().channel,
});
try {
await fsp.mkdir(path.join(workspace, 'reports'), { recursive: true });
await fsp.writeFile(path.join(workspace, 'reports', 'report.txt'), 'hi');
const session = await bridge.spawnOrAttach({ workspaceCwd: workspace });
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Report',
workspacePath: 'reports/report.txt',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;
await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content', ttlDays: 1 },
);
gcSpy.mockClear();

vi.setSystemTime(new Date('2026-07-06T00:00:00.000Z'));
await expect(
bridge.getSessionArtifacts(session.sessionId),
).resolves.toMatchObject({
artifacts: [
{
id: artifactId,
retention: 'pinned',
},
],
});
expect(gcSpy).not.toHaveBeenCalled();
} finally {
gcSpy.mockRestore();
vi.useRealTimers();
await bridge.shutdown();
if (previousQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = previousQwenHome;
}
await fsp.rm(tempHome, { recursive: true, force: true });
await fsp.rm(workspace, { recursive: true, force: true });
}
});

it('prunes expired pins during artifact mutations and reports GC warnings', async () => {
const previousQwenHome = process.env['QWEN_HOME'];
const tempHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'qwen-home-'));
const workspace = await fsp.mkdtemp(
path.join(os.tmpdir(), 'qwen-artifact-workspace-'),
);
process.env['QWEN_HOME'] = tempHome;
vi.useFakeTimers();
vi.setSystemTime(new Date('2026-07-04T00:00:00.000Z'));
const gcSpy = vi.spyOn(SessionArtifactContentStore.prototype, 'gc');
const bridge = makeBridge({
boundWorkspace: workspace,
channelFactory: async () => makeChannel().channel,
});
try {
await fsp.mkdir(path.join(workspace, 'reports'), { recursive: true });
await fsp.writeFile(path.join(workspace, 'reports', 'report.txt'), 'hi');
const session = await bridge.spawnOrAttach({ workspaceCwd: workspace });
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Report',
workspacePath: 'reports/report.txt',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;
await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content', ttlDays: 1 },
);
gcSpy.mockClear();
gcSpy.mockRejectedValueOnce(new Error('disk unavailable'));

vi.setSystemTime(new Date('2026-07-06T00:00:00.000Z'));
await expect(
bridge.addSessionArtifact(
session.sessionId,
{
title: 'New link',
url: 'https://example.com/new-link',
},
{ clientId: session.clientId },
),
).resolves.toMatchObject({
warnings: ['artifact content GC failed; stale content retained'],
});
expect(gcSpy).toHaveBeenCalledTimes(1);
await expect(
bridge.getSessionArtifacts(session.sessionId),
).resolves.toMatchObject({
artifacts: expect.arrayContaining([
expect.objectContaining({
id: artifactId,
retention: 'restorable',
persistenceWarning: 'content_expired',
}),
]),
});
} finally {
gcSpy.mockRestore();
vi.useRealTimers();
await bridge.shutdown();
if (previousQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = previousQwenHome;
}
await fsp.rm(tempHome, { recursive: true, force: true });
await fsp.rm(workspace, { recursive: true, force: true });
}
});

it('applies metadata pin options to an already pinned artifact', async () => {
const previousQwenHome = process.env['QWEN_HOME'];
const tempHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'qwen-home-'));
const workspace = await fsp.mkdtemp(
path.join(os.tmpdir(), 'qwen-artifact-workspace-'),
);
process.env['QWEN_HOME'] = tempHome;
const bridge = makeBridge({
boundWorkspace: workspace,
channelFactory: async () => makeChannel().channel,
});
try {
await fsp.mkdir(path.join(workspace, 'reports'), { recursive: true });
await fsp.writeFile(path.join(workspace, 'reports', 'report.txt'), 'hi');
const session = await bridge.spawnOrAttach({ workspaceCwd: workspace });
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Report',
workspacePath: 'reports/report.txt',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;
await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content' },
);
const gcSpy = vi.spyOn(SessionArtifactContentStore.prototype, 'gc');

await expect(
bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'metadata' },
),
).resolves.toMatchObject({
changes: [
{
action: 'updated',
artifactId,
artifact: {
retention: 'restorable',
persistenceWarning: 'metadata_only_restore',
},
},
],
});
expect(gcSpy).toHaveBeenCalledTimes(1);
} finally {
await bridge.shutdown();
if (previousQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = previousQwenHome;
}
await fsp.rm(tempHome, { recursive: true, force: true });
await fsp.rm(workspace, { recursive: true, force: true });
}
});

it('refreshes retained content on explicit content re-pin', async () => {
const previousQwenHome = process.env['QWEN_HOME'];
const tempHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'qwen-home-'));
const workspace = await fsp.mkdtemp(
path.join(os.tmpdir(), 'qwen-artifact-workspace-'),
);
process.env['QWEN_HOME'] = tempHome;
const bridge = makeBridge({
boundWorkspace: workspace,
channelFactory: async () => makeChannel().channel,
});
try {
await fsp.mkdir(path.join(workspace, 'reports'), { recursive: true });
await fsp.writeFile(
path.join(workspace, 'reports', 'report.txt'),
'first',
);
const session = await bridge.spawnOrAttach({ workspaceCwd: workspace });
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Report',
workspacePath: 'reports/report.txt',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;
const firstPin = await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content' },
);
const firstContentId =
firstPin.changes[0]?.artifact?.contentRef?.contentId;
const gcSpy = vi.spyOn(SessionArtifactContentStore.prototype, 'gc');

await fsp.writeFile(
path.join(workspace, 'reports', 'report.txt'),
'second',
);
const secondPin = await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content' },
);

expect(
secondPin.changes[0]?.artifact?.contentRef?.contentId,
).toBeTruthy();
expect(secondPin.changes[0]?.artifact?.contentRef?.contentId).not.toBe(
firstContentId,
);
expect(gcSpy).toHaveBeenCalledTimes(1);
} finally {
await bridge.shutdown();
if (previousQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = previousQwenHome;
}
await fsp.rm(tempHome, { recursive: true, force: true });
await fsp.rm(workspace, { recursive: true, force: true });
}
});

it('keeps unpin successful when best-effort content GC fails', async () => {
const previousQwenHome = process.env['QWEN_HOME'];
const tempHome = await fsp.mkdtemp(path.join(os.tmpdir(), 'qwen-home-'));
const workspace = await fsp.mkdtemp(
path.join(os.tmpdir(), 'qwen-artifact-workspace-'),
);
process.env['QWEN_HOME'] = tempHome;
const bridge = makeBridge({
boundWorkspace: workspace,
channelFactory: async () => makeChannel().channel,
});
const gcSpy = vi.spyOn(SessionArtifactContentStore.prototype, 'gc');
try {
await fsp.mkdir(path.join(workspace, 'reports'), { recursive: true });
await fsp.writeFile(path.join(workspace, 'reports', 'report.txt'), 'hi');
const session = await bridge.spawnOrAttach({ workspaceCwd: workspace });
const created = await bridge.addSessionArtifact(
session.sessionId,
{
title: 'Report',
workspacePath: 'reports/report.txt',
},
{ clientId: session.clientId },
);
const artifactId = created.changes[0]!.artifactId;
await bridge.pinSessionArtifact(
session.sessionId,
artifactId,
{ clientId: session.clientId },
{ mode: 'content' },
);
gcSpy.mockClear();
gcSpy.mockRejectedValueOnce(new Error('disk unavailable'));

await expect(
bridge.unpinSessionArtifact(session.sessionId, artifactId, {
clientId: session.clientId,
}),
).resolves.toMatchObject({
changes: [{ action: 'updated', artifactId }],
warnings: ['content_delete_preserved'],
});
expect(gcSpy).toHaveBeenCalledTimes(1);
} finally {
gcSpy.mockRestore();
await bridge.shutdown();
if (previousQwenHome === undefined) {
delete process.env['QWEN_HOME'];
} else {
process.env['QWEN_HOME'] = previousQwenHome;
}
await fsp.rm(tempHome, { recursive: true, force: true });
await fsp.rm(workspace, { recursive: true, force: true });
}
});

it('uses bridge telemetry for channel/session/prompt dispatch and prompt metadata injection', async () => {
const handle = makeChannel();
const operations: string[] = [];
Expand Down Expand Up @@ -7088,6 +7447,11 @@ describe('createAcpSessionBridge', () => {
{ clientId: 'client-not-issued' },
),
).rejects.toBeInstanceOf(InvalidClientIdError);
await expect(
bridge.fsckSessionArtifacts(session.sessionId, {
clientId: 'client-not-issued',
}),
).rejects.toBeInstanceOf(InvalidClientIdError);
await bridge.shutdown();
});

Expand Down
Loading
Loading