From b96b03763bdebcdc1577a047ac7ef41a4ac71737 Mon Sep 17 00:00:00 2001 From: John Hardy Date: Wed, 4 Feb 2026 11:34:15 +1100 Subject: [PATCH 1/6] test(e2e): add adapter harness and simple fixture --- package.json | 4 +- tests/e2e/adapter/adapter.e2e.test.ts | 109 ++++++++++ tests/e2e/adapter/dap-client.ts | 193 ++++++++++++++++++ .../e2e/fixtures/simple/.vscode/debug80.json | 22 ++ tests/e2e/fixtures/simple/.vscode/launch.json | 13 ++ tests/e2e/fixtures/simple/src/simple.asm | 5 + vitest.e2e.config.ts | 10 + 7 files changed, 355 insertions(+), 1 deletion(-) create mode 100644 tests/e2e/adapter/adapter.e2e.test.ts create mode 100644 tests/e2e/adapter/dap-client.ts create mode 100644 tests/e2e/fixtures/simple/.vscode/debug80.json create mode 100644 tests/e2e/fixtures/simple/.vscode/launch.json create mode 100644 tests/e2e/fixtures/simple/src/simple.asm create mode 100644 vitest.e2e.config.ts diff --git a/package.json b/package.json index 311e2b6..6fbbc17 100644 --- a/package.json +++ b/package.json @@ -289,7 +289,9 @@ "test:watch": "vitest", "coverage": "vitest run --coverage", "perf:z80": "yarn build && node scripts/perf-z80.js", - "package": "vsce package" + "package": "vsce package", + "test:e2e:adapter": "vitest run -c vitest.e2e.config.ts", + "test:e2e": "yarn test:e2e:adapter" }, "devDependencies": { "@types/node": "^20.10.0", diff --git a/tests/e2e/adapter/adapter.e2e.test.ts b/tests/e2e/adapter/adapter.e2e.test.ts new file mode 100644 index 0000000..7018b85 --- /dev/null +++ b/tests/e2e/adapter/adapter.e2e.test.ts @@ -0,0 +1,109 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { PassThrough } from 'stream'; +import path from 'path'; +import { Z80DebugSession } from '../../../src/debug/adapter'; +import { DapClient } from './dap-client'; + +const fixtureRoot = path.resolve(__dirname, '../fixtures/simple'); +const projectConfig = path.join(fixtureRoot, '.vscode', 'debug80.json'); +const sourcePath = path.join(fixtureRoot, 'src', 'simple.asm'); + +const THREAD_ID = 1; + +type SessionHarness = { + session: Z80DebugSession; + client: DapClient; + input: PassThrough; + output: PassThrough; +}; + +function createHarness(): SessionHarness { + const input = new PassThrough(); + const output = new PassThrough(); + const session = new Z80DebugSession(); + session.setRunAsServer(true); + session.start(input, output); + const client = new DapClient(input, output); + return { session, client, input, output }; +} + +async function initialize(client: DapClient): Promise { + await client.sendRequest('initialize', { + adapterID: 'z80', + pathFormat: 'path', + linesStartAt1: true, + columnsStartAt1: true, + }); + await client.waitForEvent('initialized'); +} + +describe('Debug80 adapter e2e (in-process)', () => { + let harness: SessionHarness | undefined; + + beforeEach(() => { + harness = createHarness(); + }); + + afterEach(() => { + if (!harness) { + return; + } + harness.client.dispose(); + harness.input.end(); + harness.output.end(); + harness = undefined; + }); + + it('stops on entry when stopOnEntry is true', async () => { + const { client } = harness ?? createHarness(); + + await initialize(client); + await client.sendRequest('launch', { + projectConfig, + target: 'app', + stopOnEntry: true, + openRomSourcesOnLaunch: false, + openMainSourceOnLaunch: false, + }); + await client.sendRequest('setBreakpoints', { + source: { path: sourcePath }, + breakpoints: [], + }); + await client.sendRequest('configurationDone'); + + const stopped = await client.waitForEvent<{ body?: { reason?: string } }>('stopped'); + expect(stopped.body?.reason).toBe('entry'); + + await client.sendRequest('disconnect'); + }); + + it('hits a source breakpoint after configurationDone', async () => { + const { client } = harness ?? createHarness(); + + await initialize(client); + await client.sendRequest('launch', { + projectConfig, + target: 'app', + stopOnEntry: false, + openRomSourcesOnLaunch: false, + openMainSourceOnLaunch: false, + }); + await client.sendRequest('setBreakpoints', { + source: { path: sourcePath }, + breakpoints: [{ line: 2 }], + }); + await client.sendRequest('configurationDone'); + + const stopped = await client.waitForEvent<{ body?: { reason?: string } }>('stopped'); + expect(stopped.body?.reason).toBe('breakpoint'); + + const stack = await client.sendRequest<{ + body?: { stackFrames?: Array<{ line: number; source?: { path?: string } }> }; + }>('stackTrace', { threadId: THREAD_ID, startFrame: 0, levels: 1 }); + const frame = stack.body?.stackFrames?.[0]; + expect(frame?.line).toBe(2); + expect(frame?.source?.path).toBe(sourcePath); + + await client.sendRequest('disconnect'); + }); +}); diff --git a/tests/e2e/adapter/dap-client.ts b/tests/e2e/adapter/dap-client.ts new file mode 100644 index 0000000..7674a05 --- /dev/null +++ b/tests/e2e/adapter/dap-client.ts @@ -0,0 +1,193 @@ +import { Readable, Writable } from 'stream'; + +type DapMessage = { + seq: number; + type: 'request' | 'response' | 'event'; + command?: string; + event?: string; + request_seq?: number; + success?: boolean; + message?: string; + body?: unknown; +}; + +type PendingRequest = { + resolve: (value: DapMessage) => void; + reject: (reason?: Error) => void; + timer?: NodeJS.Timeout; +}; + +type PendingEvent = { + event: string; + predicate?: (payload: DapMessage) => boolean; + resolve: (value: DapMessage) => void; + reject: (reason?: Error) => void; + timer?: NodeJS.Timeout; +}; + +export class DapClient { + private readonly input: Writable; + private readonly output: Readable; + private readonly pending = new Map(); + private readonly eventWaiters: PendingEvent[] = []; + private readonly eventQueue: DapMessage[] = []; + private buffer = Buffer.alloc(0); + private seq = 1; + + constructor(input: Writable, output: Readable) { + this.input = input; + this.output = output; + this.output.on('data', (chunk) => this.onData(chunk)); + } + + async sendRequest( + command: string, + args?: Record, + timeoutMs = 5000 + ): Promise { + const seq = this.seq++; + const request: DapMessage & { arguments?: Record } = { + seq, + type: 'request', + command, + ...(args ? { arguments: args } : {}), + }; + this.writeMessage(request); + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + this.pending.delete(seq); + reject(new Error(`Timeout waiting for response to ${command}`)); + }, timeoutMs); + this.pending.set(seq, { + resolve: (msg) => resolve(msg as T), + reject, + timer, + }); + }); + } + + waitForEvent( + event: string, + predicate?: (payload: DapMessage) => boolean, + timeoutMs = 5000 + ): Promise { + const existingIndex = this.eventQueue.findIndex((msg) => { + if (msg.type !== 'event' || msg.event !== event) { + return false; + } + return predicate ? predicate(msg) : true; + }); + if (existingIndex >= 0) { + const [msg] = this.eventQueue.splice(existingIndex, 1); + return Promise.resolve(msg as T); + } + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + const idx = this.eventWaiters.indexOf(waiter); + if (idx >= 0) { + this.eventWaiters.splice(idx, 1); + } + reject(new Error(`Timeout waiting for event ${event}`)); + }, timeoutMs); + const waiter: PendingEvent = { + event, + predicate, + resolve: (msg) => resolve(msg as T), + reject, + timer, + }; + this.eventWaiters.push(waiter); + }); + } + + dispose(): void { + for (const [, pending] of this.pending) { + if (pending.timer) { + clearTimeout(pending.timer); + } + pending.reject(new Error('Client disposed')); + } + this.pending.clear(); + for (const waiter of this.eventWaiters) { + if (waiter.timer) { + clearTimeout(waiter.timer); + } + waiter.reject(new Error('Client disposed')); + } + this.eventWaiters.splice(0, this.eventWaiters.length); + } + + private writeMessage(message: DapMessage & { arguments?: unknown }): void { + const json = JSON.stringify(message); + const header = `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n`; + this.input.write(header + json, 'utf8'); + } + + private onData(chunk: Buffer): void { + this.buffer = Buffer.concat([this.buffer, chunk]); + while (true) { + const headerEnd = this.buffer.indexOf('\r\n\r\n'); + if (headerEnd === -1) { + return; + } + const header = this.buffer.slice(0, headerEnd).toString('utf8'); + const match = /Content-Length: (\d+)/i.exec(header); + if (!match) { + this.buffer = this.buffer.slice(headerEnd + 4); + continue; + } + const length = Number.parseInt(match[1] ?? '0', 10); + const bodyStart = headerEnd + 4; + const bodyEnd = bodyStart + length; + if (this.buffer.length < bodyEnd) { + return; + } + const body = this.buffer.slice(bodyStart, bodyEnd).toString('utf8'); + this.buffer = this.buffer.slice(bodyEnd); + const message = JSON.parse(body) as DapMessage; + this.handleMessage(message); + } + } + + private handleMessage(message: DapMessage): void { + if (message.type === 'response') { + const requestSeq = message.request_seq; + if (requestSeq === undefined) { + return; + } + const pending = this.pending.get(requestSeq); + if (!pending) { + return; + } + if (pending.timer) { + clearTimeout(pending.timer); + } + this.pending.delete(requestSeq); + if (message.success === false) { + pending.reject(new Error(message.message ?? 'DAP error response')); + } else { + pending.resolve(message); + } + return; + } + + if (message.type === 'event') { + const waiterIndex = this.eventWaiters.findIndex((waiter) => { + if (waiter.event !== message.event) { + return false; + } + return waiter.predicate ? waiter.predicate(message) : true; + }); + if (waiterIndex >= 0) { + const waiter = this.eventWaiters.splice(waiterIndex, 1)[0]; + if (waiter.timer) { + clearTimeout(waiter.timer); + } + waiter.resolve(message); + return; + } + this.eventQueue.push(message); + } + } +} diff --git a/tests/e2e/fixtures/simple/.vscode/debug80.json b/tests/e2e/fixtures/simple/.vscode/debug80.json new file mode 100644 index 0000000..681347b --- /dev/null +++ b/tests/e2e/fixtures/simple/.vscode/debug80.json @@ -0,0 +1,22 @@ +{ + "defaultTarget": "app", + "targets": { + "app": { + "sourceFile": "src/simple.asm", + "outputDir": "build", + "artifactBase": "simple", + "platform": "simple", + "assemble": false, + "hex": "build/simple.hex", + "listing": "build/simple.lst", + "simple": { + "regions": [ + { "start": 0, "end": 2047, "kind": "rom" }, + { "start": 2048, "end": 65535, "kind": "ram" } + ], + "appStart": 0, + "entry": 0 + } + } + } +} diff --git a/tests/e2e/fixtures/simple/.vscode/launch.json b/tests/e2e/fixtures/simple/.vscode/launch.json new file mode 100644 index 0000000..5234c3e --- /dev/null +++ b/tests/e2e/fixtures/simple/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "z80", + "request": "launch", + "name": "Debug80 E2E (Simple)", + "projectConfig": "${workspaceFolder}/.vscode/debug80.json", + "target": "app", + "stopOnEntry": true + } + ] +} diff --git a/tests/e2e/fixtures/simple/src/simple.asm b/tests/e2e/fixtures/simple/src/simple.asm new file mode 100644 index 0000000..d59f74b --- /dev/null +++ b/tests/e2e/fixtures/simple/src/simple.asm @@ -0,0 +1,5 @@ +START: + NOP + IN A,(TERM_STATUS) +VALUE: EQU $0000 + JP START diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts new file mode 100644 index 0000000..26bca4c --- /dev/null +++ b/vitest.e2e.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['tests/e2e/**/*.test.ts'], + testTimeout: 20000, + hookTimeout: 20000, + }, +}); From 3e2fb3590be219f2313843427a640b339f3bbbaa Mon Sep 17 00:00:00 2001 From: John Hardy Date: Wed, 4 Feb 2026 11:35:34 +1100 Subject: [PATCH 2/6] test(e2e): add fixture build artifacts --- .gitignore | 4 ++ .../fixtures/simple/build/simple.d8dbg.json | 56 +++++++++++++++++++ tests/e2e/fixtures/simple/build/simple.hex | 2 + tests/e2e/fixtures/simple/build/simple.lst | 7 +++ 4 files changed, 69 insertions(+) create mode 100644 tests/e2e/fixtures/simple/build/simple.d8dbg.json create mode 100644 tests/e2e/fixtures/simple/build/simple.hex create mode 100644 tests/e2e/fixtures/simple/build/simple.lst diff --git a/.gitignore b/.gitignore index cd82f14..63eb7c8 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,7 @@ tests/**/*.map *.tmp *.temp .debug80/ + +# E2E fixtures +!tests/e2e/fixtures/**/build/ +!tests/e2e/fixtures/**/build/* diff --git a/tests/e2e/fixtures/simple/build/simple.d8dbg.json b/tests/e2e/fixtures/simple/build/simple.d8dbg.json new file mode 100644 index 0000000..b52fc2e --- /dev/null +++ b/tests/e2e/fixtures/simple/build/simple.d8dbg.json @@ -0,0 +1,56 @@ +{ + "format": "d8-debug-map", + "version": 1, + "arch": "z80", + "addressWidth": 16, + "endianness": "little", + "files": { + "src/simple.asm": { + "segments": [ + { + "start": 0, + "end": 1, + "line": 2, + "lstLine": 2, + "lstText": "NOP", + "kind": "code", + "confidence": "high" + }, + { + "start": 1, + "end": 3, + "line": 3, + "lstLine": 3, + "lstText": "IN A,(TERM_STATUS)", + "kind": "code", + "confidence": "high" + }, + { + "start": 3, + "end": 6, + "line": 5, + "lstLine": 5, + "lstText": "JP START", + "kind": "code", + "confidence": "high" + } + ], + "symbols": [ + { + "name": "START", + "address": 0, + "line": 1, + "kind": "label", + "scope": "global" + }, + { + "name": "VALUE", + "address": 3, + "line": 4, + "kind": "constant", + "scope": "global" + } + ] + } + } +} diff --git a/tests/e2e/fixtures/simple/build/simple.hex b/tests/e2e/fixtures/simple/build/simple.hex new file mode 100644 index 0000000..3793dc2 --- /dev/null +++ b/tests/e2e/fixtures/simple/build/simple.hex @@ -0,0 +1,2 @@ +:0600000000DB02C300005A +:00000001FF diff --git a/tests/e2e/fixtures/simple/build/simple.lst b/tests/e2e/fixtures/simple/build/simple.lst new file mode 100644 index 0000000..b6cd9d3 --- /dev/null +++ b/tests/e2e/fixtures/simple/build/simple.lst @@ -0,0 +1,7 @@ +0000 START: +0000 00 NOP +0001 DB 02 IN A,(TERM_STATUS) +0003 VALUE: EQU $0000 +0003 C3 00 00 JP START +START: 0000 DEFINED AT LINE 1 IN simple.asm +VALUE: 0003 DEFINED AT LINE 4 IN simple.asm From e061dbefbde632869520351fd4ebf1d243fdbedd Mon Sep 17 00:00:00 2001 From: John Hardy Date: Wed, 4 Feb 2026 12:22:30 +1100 Subject: [PATCH 3/6] test(e2e): align fixture workspace --- tests/e2e/adapter/adapter.e2e.test.ts | 25 +++++++++++++++++-- tests/e2e/adapter/dap-client.ts | 10 +++++++- tests/e2e/adapter/vscode-mock.ts | 19 ++++++++++++++ .../e2e/fixtures/simple/.vscode/debug80.json | 17 ++++++++++--- vitest.e2e.config.ts | 14 +++++++++++ 5 files changed, 79 insertions(+), 6 deletions(-) create mode 100644 tests/e2e/adapter/vscode-mock.ts diff --git a/tests/e2e/adapter/adapter.e2e.test.ts b/tests/e2e/adapter/adapter.e2e.test.ts index 7018b85..cee3694 100644 --- a/tests/e2e/adapter/adapter.e2e.test.ts +++ b/tests/e2e/adapter/adapter.e2e.test.ts @@ -3,6 +3,7 @@ import { PassThrough } from 'stream'; import path from 'path'; import { Z80DebugSession } from '../../../src/debug/adapter'; import { DapClient } from './dap-client'; +import { workspace } from './vscode-mock'; const fixtureRoot = path.resolve(__dirname, '../fixtures/simple'); const projectConfig = path.join(fixtureRoot, '.vscode', 'debug80.json'); @@ -37,10 +38,30 @@ async function initialize(client: DapClient): Promise { await client.waitForEvent('initialized'); } +async function launchWithDiagnostics( + client: DapClient, + args: Record +): Promise { + try { + await client.sendRequest('launch', args); + } catch (err) { + let output = ''; + try { + const event = await client.waitForEvent<{ body?: { output?: string } }>('output', undefined, 1000); + output = event.body?.output?.trim() ?? ''; + } catch { + // ignore missing output + } + const message = err instanceof Error ? err.message : String(err); + const detail = output.length > 0 ? `${message}\n${output}` : message; + throw new Error(detail); + } +} describe('Debug80 adapter e2e (in-process)', () => { let harness: SessionHarness | undefined; beforeEach(() => { + workspace.workspaceFolders = [{ uri: { fsPath: fixtureRoot } }]; harness = createHarness(); }); @@ -58,7 +79,7 @@ describe('Debug80 adapter e2e (in-process)', () => { const { client } = harness ?? createHarness(); await initialize(client); - await client.sendRequest('launch', { + await launchWithDiagnostics(client, { projectConfig, target: 'app', stopOnEntry: true, @@ -81,7 +102,7 @@ describe('Debug80 adapter e2e (in-process)', () => { const { client } = harness ?? createHarness(); await initialize(client); - await client.sendRequest('launch', { + await launchWithDiagnostics(client, { projectConfig, target: 'app', stopOnEntry: false, diff --git a/tests/e2e/adapter/dap-client.ts b/tests/e2e/adapter/dap-client.ts index 7674a05..8beda58 100644 --- a/tests/e2e/adapter/dap-client.ts +++ b/tests/e2e/adapter/dap-client.ts @@ -52,7 +52,6 @@ export class DapClient { command, ...(args ? { arguments: args } : {}), }; - this.writeMessage(request); return new Promise((resolve, reject) => { const timer = setTimeout(() => { this.pending.delete(seq); @@ -63,6 +62,15 @@ export class DapClient { reject, timer, }); + try { + this.writeMessage(request); + } catch (err) { + if (timer) { + clearTimeout(timer); + } + this.pending.delete(seq); + reject(err instanceof Error ? err : new Error(String(err))); + } }); } diff --git a/tests/e2e/adapter/vscode-mock.ts b/tests/e2e/adapter/vscode-mock.ts new file mode 100644 index 0000000..fce3c2d --- /dev/null +++ b/tests/e2e/adapter/vscode-mock.ts @@ -0,0 +1,19 @@ +export type WorkspaceFolder = { uri: { fsPath: string } }; + +export const workspace: { workspaceFolders?: WorkspaceFolder[] } = { + workspaceFolders: undefined, +}; + +export const commands = { + executeCommand: async (_command: string): Promise => false, +}; + +export class DebugAdapterInlineImplementation { + constructor(_session: unknown) { + // no-op stub for tests + } +} + +export type ProviderResult = T | undefined | null | Promise; + +export type DebugSession = unknown; diff --git a/tests/e2e/fixtures/simple/.vscode/debug80.json b/tests/e2e/fixtures/simple/.vscode/debug80.json index 681347b..5a8618e 100644 --- a/tests/e2e/fixtures/simple/.vscode/debug80.json +++ b/tests/e2e/fixtures/simple/.vscode/debug80.json @@ -11,12 +11,23 @@ "listing": "build/simple.lst", "simple": { "regions": [ - { "start": 0, "end": 2047, "kind": "rom" }, - { "start": 2048, "end": 65535, "kind": "ram" } + { + "start": 0, + "end": 2047, + "kind": "rom" + }, + { + "start": 2048, + "end": 65535, + "kind": "ram" + } ], "appStart": 0, "entry": 0 - } + }, + "sourceRoots": [ + "." + ] } } } diff --git a/vitest.e2e.config.ts b/vitest.e2e.config.ts index 26bca4c..33fc0e1 100644 --- a/vitest.e2e.config.ts +++ b/vitest.e2e.config.ts @@ -1,10 +1,24 @@ import { defineConfig } from 'vitest/config'; +import path from 'path'; + +const cacheDir = path.resolve( + process.env.TMPDIR ?? '/tmp', + 'debug80-vitest-e2e-cache' +); export default defineConfig({ + resolve: { + alias: { + vscode: path.resolve(__dirname, 'tests/e2e/adapter/vscode-mock.ts'), + }, + }, test: { environment: 'node', include: ['tests/e2e/**/*.test.ts'], testTimeout: 20000, hookTimeout: 20000, + cache: { + dir: cacheDir, + }, }, }); From 142a48e279305d9f1dcb1a0d270097f0bce48d0b Mon Sep 17 00:00:00 2001 From: John Hardy Date: Wed, 4 Feb 2026 13:23:53 +1100 Subject: [PATCH 4/6] chore: restore webview build assets --- tests/e2e/adapter/dap-client.ts | 13 ++++++------- tests/e2e/adapter/vscode-mock.ts | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/e2e/adapter/dap-client.ts b/tests/e2e/adapter/dap-client.ts index 8beda58..86b1a31 100644 --- a/tests/e2e/adapter/dap-client.ts +++ b/tests/e2e/adapter/dap-client.ts @@ -37,7 +37,7 @@ export class DapClient { constructor(input: Writable, output: Readable) { this.input = input; this.output = output; - this.output.on('data', (chunk) => this.onData(chunk)); + this.output.on('data', (chunk: Buffer | string | Uint8Array) => this.onData(chunk)); } async sendRequest( @@ -65,9 +65,7 @@ export class DapClient { try { this.writeMessage(request); } catch (err) { - if (timer) { - clearTimeout(timer); - } + clearTimeout(timer); this.pending.delete(seq); reject(err instanceof Error ? err : new Error(String(err))); } @@ -132,9 +130,10 @@ export class DapClient { this.input.write(header + json, 'utf8'); } - private onData(chunk: Buffer): void { - this.buffer = Buffer.concat([this.buffer, chunk]); - while (true) { + private onData(chunk: Buffer | string | Uint8Array): void { + const next = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + this.buffer = Buffer.concat([this.buffer, next]); + while (this.buffer.length > 0) { const headerEnd = this.buffer.indexOf('\r\n\r\n'); if (headerEnd === -1) { return; diff --git a/tests/e2e/adapter/vscode-mock.ts b/tests/e2e/adapter/vscode-mock.ts index fce3c2d..e4f3a07 100644 --- a/tests/e2e/adapter/vscode-mock.ts +++ b/tests/e2e/adapter/vscode-mock.ts @@ -5,7 +5,7 @@ export const workspace: { workspaceFolders?: WorkspaceFolder[] } = { }; export const commands = { - executeCommand: async (_command: string): Promise => false, + executeCommand: (_command: string): Promise => Promise.resolve(false), }; export class DebugAdapterInlineImplementation { From 93afd0156d2c36771d9e10c5dd2965d8e02c4b08 Mon Sep 17 00:00:00 2001 From: John Hardy Date: Sun, 8 Feb 2026 20:32:11 +1100 Subject: [PATCH 5/6] test: exclude e2e adapter tests from unit run --- vitest.config.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vitest.config.ts b/vitest.config.ts index 8d7dc18..28a6606 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -1,9 +1,10 @@ -import { defineConfig } from 'vitest/config'; +import { configDefaults, defineConfig } from 'vitest/config'; export default defineConfig({ test: { environment: 'node', include: ['tests/**/*.test.ts'], + exclude: ['tests/e2e/**', ...configDefaults.exclude], coverage: { provider: 'v8', reporter: ['text', 'html', 'lcov'], From 84ad6c7a1a26e53b78f6c7dad5ef3ac353253ba3 Mon Sep 17 00:00:00 2001 From: John Hardy Date: Sun, 8 Feb 2026 20:41:28 +1100 Subject: [PATCH 6/6] fix(tec1g): decode initial SYS_CTRL state Initialize protect/expand/shadow flags from SYS_CTRL so SYS_INPUT reflects protectOnReset and other latched bits. --- src/platforms/tec1g/runtime.ts | 11 ++++++----- tests/extension/extension.test.ts | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/platforms/tec1g/runtime.ts b/src/platforms/tec1g/runtime.ts index 69eed60..a90fafa 100644 --- a/src/platforms/tec1g/runtime.ts +++ b/src/platforms/tec1g/runtime.ts @@ -347,6 +347,7 @@ export function createTec1gRuntime( const initialSysCtrl = (config.expansionBankHi ? TEC1G_SYSCTRL_BANK_A14 : 0) | (config.protectOnReset ? TEC1G_SYSCTRL_PROTECT : 0); + const initialSysCtrlDecoded = decodeSysCtrl(initialSysCtrl); const matrixMode = config.matrixMode; const rtcEnabled = config.rtcEnabled; const rtc = rtcEnabled ? new Ds1302() : null; @@ -425,11 +426,11 @@ export function createTec1gRuntime( updateMs: config.updateMs, yieldMs: config.yieldMs, sysCtrl: initialSysCtrl, - shadowEnabled: true, - protectEnabled: false, - expandEnabled: false, - bankA14: config.expansionBankHi, - capsLock: false, + shadowEnabled: initialSysCtrlDecoded.shadowEnabled, + protectEnabled: initialSysCtrlDecoded.protectEnabled, + expandEnabled: initialSysCtrlDecoded.expandEnabled, + bankA14: initialSysCtrlDecoded.bankA14, + capsLock: initialSysCtrlDecoded.capsLock, cartridgePresent: cartridgePresentDefault, shiftKeyActive: false, rawKeyActive: false, diff --git a/tests/extension/extension.test.ts b/tests/extension/extension.test.ts index 53182c7..15cd06c 100644 --- a/tests/extension/extension.test.ts +++ b/tests/extension/extension.test.ts @@ -88,7 +88,7 @@ describe('extension activation', () => { expect(registerCommand).toHaveBeenCalledWith('debug80.createProject', expect.anything()); expect(registerCommand).toHaveBeenCalledWith('debug80.openTerminal', expect.anything()); expect(context.subscriptions.length).toBeGreaterThan(0); - }); + }, 20000); it('forces asm documents to asm-collection when available', async () => { const extension = (await import('../../src/extension/extension')) as { @@ -109,5 +109,5 @@ describe('extension activation', () => { const docValue = doc as { uri?: { path?: string } }; expect(docValue.uri?.path).toBe('/tmp/test.asm'); expect(languageId).toBe('asm-collection'); - }); + }, 20000); });